@expokit/cli 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib.mjs +1 -1
- package/package.json +1 -1
- package/registry/_docs/accordion.json +1 -1
- package/registry/_docs/avatar.json +6 -6
- package/registry/_docs/badge.json +3 -3
- package/registry/_docs/bottom-nav.json +6 -6
- package/registry/_docs/bottom-sheet.json +3 -3
- package/registry/_docs/bullet.json +6 -0
- package/registry/_docs/button-3d.json +2 -2
- package/registry/_docs/button.json +2 -2
- package/registry/_docs/calendar.json +5 -5
- package/registry/_docs/card.json +1 -1
- package/registry/_docs/chip-group.json +1 -1
- package/registry/_docs/confetti.json +3 -3
- package/registry/_docs/date-picker.json +4 -4
- package/registry/_docs/drawer.json +4 -4
- package/registry/_docs/flip-card.json +4 -4
- package/registry/_docs/floating-input.json +1 -1
- package/registry/_docs/grain.json +2 -2
- package/registry/_docs/isometric-bar.json +6 -0
- package/registry/_docs/liquidglass-tab.json +1 -1
- package/registry/_docs/otp-input.json +1 -1
- package/registry/_docs/payment-card.json +3 -3
- package/registry/_docs/pixel-bar.json +6 -0
- package/registry/_docs/polar-area.json +6 -0
- package/registry/_docs/progress-steps.json +4 -4
- package/registry/_docs/progress.json +2 -2
- package/registry/_docs/radio-group.json +1 -1
- package/registry/_docs/search-bar.json +3 -3
- package/registry/_docs/segmented-control.json +1 -1
- package/registry/_docs/select.json +6 -6
- package/registry/_docs/skeleton.json +1 -1
- package/registry/_docs/slider.json +6 -6
- package/registry/_docs/spinner.json +2 -2
- package/registry/_docs/stateful-button.json +1 -1
- package/registry/_docs/stepper.json +3 -3
- package/registry/_docs/tabs.json +2 -2
- package/registry/_docs/text-animation.json +4 -4
- package/registry/_docs/textarea.json +1 -1
- package/registry/_docs/tilt-card.json +3 -3
- package/registry/_docs/time-picker.json +6 -6
- package/registry/_docs/timeline-chart.json +6 -0
- package/registry/_docs/timeline.json +4 -4
- package/registry/_docs/treemap.json +6 -0
- package/registry/_docs/waterfall.json +6 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +31 -3
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +4 -1
- package/registry/alert/files/src/components/ui/alert.tsx +5 -2
- package/registry/alert/files/src/components/ui/button.tsx +32 -4
- package/registry/badge/files/src/components/ui/badge.tsx +2 -2
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +11 -5
- package/registry/button/files/src/components/ui/button.tsx +32 -4
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +33 -6
- package/registry/calendar/files/src/components/ui/calendar.tsx +62 -17
- package/registry/card/files/src/components/ui/card.tsx +25 -5
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +26 -4
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +35 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +45 -21
- package/registry/coupon/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/date-picker/files/src/components/ui/calendar.tsx +62 -17
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +96 -42
- package/registry/dialog/files/src/components/ui/dialog.tsx +9 -5
- package/registry/drawer/files/src/components/ui/drawer.tsx +8 -4
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +75 -26
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
- package/registry/list-item/files/src/components/ui/list-item.tsx +10 -3
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +50 -19
- package/registry/password-input/files/src/components/ui/floating-input.tsx +75 -26
- package/registry/password-input/files/src/components/ui/password-input.tsx +53 -19
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +19 -15
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/progress/files/src/components/ui/progress.tsx +9 -2
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +11 -4
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +39 -9
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +70 -17
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +37 -9
- package/registry/select/files/src/components/ui/select.tsx +158 -83
- package/registry/slider/files/src/components/ui/slider.tsx +53 -25
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
- package/registry/stepper/files/src/components/ui/stepper.tsx +76 -26
- package/registry/switch/files/src/components/ui/switch.tsx +56 -19
- package/registry/tabs/files/src/components/ui/tabs.tsx +7 -4
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +7 -2
- package/registry/textarea/files/src/components/ui/textarea.tsx +104 -36
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +154 -73
- package/registry/timeline/files/src/components/ui/timeline.tsx +9 -4
- package/registry/toast/files/src/components/ui/button.tsx +32 -4
- package/registry/toast/files/src/components/ui/toast.tsx +15 -6
|
@@ -178,22 +178,22 @@
|
|
|
178
178
|
{
|
|
179
179
|
"name": "AppNavigation",
|
|
180
180
|
"title": "App Navigation",
|
|
181
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type ViewProps } from 'react-native';\nimport {\n Drawer,\n DrawerFooter,\n DrawerHeader,\n DrawerIcon,\n DrawerItem,\n DrawerProfile,\n DrawerSection,\n useDrawer,\n type DrawerIconName,\n} from '@/components/ui/drawer';\nimport { alpha, useKit } from '@/lib/kit';\n\nconst STATUS_H = 40;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\n/** True when the demo fills a real phone screen: the frame and the fake status bar step aside. */\nconst ScreenMode = React.createContext(false);\n\n/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, screen = false, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <ScreenMode.Provider value={screen}>\n <View\n style={[\n { backgroundColor: colors.background, overflow: 'hidden' },\n screen ? { flex: 1 } : { height: 600, borderRadius: radius.scale(28), borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border },\n style,\n ]}\n {...rest}\n />\n </ScreenMode.Provider>\n );\n}\n\n/** Fake status bar: time, signal and battery, drawn with Views. */\nfunction StatusBar() {\n const { colors, radius } = useKit();\n const ink = colors.foreground;\n return (\n <View style={{ height: STATUS_H, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24 }}>\n <Text style={{ fontSize: 15, color: ink, fontVariant: ['tabular-nums'] }}>9:41</Text>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2 }}>\n {[4, 6, 8, 10].map((h) => (\n <View key={h} style={{ width: 3, height: h, borderRadius: radius.capsule(3), backgroundColor: ink }} />\n ))}\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>\n <View style={{ width: 22, height: 11, padding: 1.5, borderRadius: radius.scale(3.5), borderWidth: 1, borderColor: alpha(ink, 0.4) }}>\n <View style={{ width: '72%', flex: 1, borderRadius: radius.scale(2), backgroundColor: ink }} />\n </View>\n <View style={{ width: 1.5, height: 4, borderRadius: 1, backgroundColor: alpha(ink, 0.4) }} />\n </View>\n </View>\n </View>\n );\n}\n\n/** The app screen behind the drawer: status bar plus your content. */\nfunction Phone({ children }: { children: React.ReactNode }) {\n const { colors } = useKit();\n const screen = React.useContext(ScreenMode);\n return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n {screen ? null : <StatusBar />}\n {children}\n </View>\n );\n}\n\nfunction IconButton({ icon, label, onPress, color }: { icon: DrawerIconName; label: string; onPress?: () => void; color?: string }) {\n const { colors, radius } = useKit();\n return (\n <Pressable\n onPress={onPress}\n accessibilityRole=\"button\"\n accessibilityLabel={label}\n style={({ pressed }) => ({\n width: 44,\n height: 44,\n borderRadius: radius.capsule(44),\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: pressed ? colors.muted : 'transparent',\n })}\n >\n <DrawerIcon name={icon} size={22} color={color ?? colors.foreground} />\n </Pressable>\n );\n}\n\n/** The hamburger: toggles the surrounding Drawer through useDrawer(). */\nfunction MenuButton() {\n const { open, setOpen } = useDrawer();\n return <IconButton icon=\"menu\" label=\"Open navigation\" onPress={() => setOpen(!open)} />;\n}\n\n/** A plain inline top bar: hamburger, title (+ subtitle) and trailing actions. */\nfunction TopBar({ title, subtitle, right, menu = true }: { title: string; subtitle?: string; right?: React.ReactNode; menu?: boolean }) {\n const { colors } = useKit();\n return (\n <View style={{ height: 56, flexDirection: 'row', alignItems: 'center', paddingLeft: menu ? 6 : 16, paddingRight: 8, gap: 4 }}>\n {menu ? <MenuButton /> : null}\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text numberOfLines={1} style={{ fontSize: subtitle ? 19 : 22, letterSpacing: -0.4, color: colors.foreground }}>\n {title}\n </Text>\n {subtitle ? (\n <Text numberOfLines={1} style={{ fontSize: 12.5, color: colors.mutedForeground }}>\n {subtitle}\n </Text>\n ) : null}\n </View>\n {right}\n </View>\n );\n}\n\nfunction Initials({ name, size = 32, square, ring }: { name: string; size?: number; square?: boolean; ring?: string }) {\n const { colors, radius } = useKit();\n const letters = name\n .split(/\\s+/)\n .slice(0, 2)\n .map((w) => w[0])\n .join('');\n return (\n <View\n style={{\n width: size,\n height: size,\n borderRadius: square ? radius.scale(size * 0.28) : radius.capsule(size),\n backgroundColor: square ? colors.foreground : alpha(colors.accent, 0.14),\n borderWidth: ring ? 2 : 0,\n borderColor: ring,\n alignItems: 'center',\n justifyContent: 'center',\n }}\n >\n <Text style={{ fontSize: size * 0.38, color: square ? colors.background : colors.accent }}>{letters}</Text>\n </View>\n );\n}\n\nfunction Dot({ color, size = 10 }: { color: string; size?: number }) {\n const { radius } = useKit();\n return <View style={{ width: size, height: size, borderRadius: radius.capsule(size), backgroundColor: color }} />;\n}\n\n/* Mail ------------------------------------------------------------------------------------------ */\n\nconst MAIL = [\n { from: 'Lena Fischer', subject: 'Offsite agenda — final draft', snippet: 'Attached the plan for Thursday. Shout if the 10:00 slot clashes.', time: '9:41', unread: true },\n { from: 'GitHub', service: true, subject: '[northwind/app] PR #482 merged', snippet: 'feat: offline drafts for the editor (#482) was merged into main.', time: '9:12', unread: true },\n { from: 'Omar Haddad', subject: 'Re: Invoice 2041', snippet: 'Paid this morning — the receipt is in the thread below.', time: '8:03', unread: true },\n { from: 'Figma', service: true, subject: 'Priya commented on Checkout v3', snippet: '“Can we try the sheet at 90% height here?”', time: 'Tue' },\n { from: 'Stripe', service: true, subject: 'Your October payout is on its way', snippet: '₹2,48,000 will arrive in your account in 2 business days.', time: 'Mon' },\n { from: 'Jonas Weber', subject: 'Photos from Lisbon', snippet: 'Finally sorted them — 48 shots, the tram ones came out great.', time: 'Sun' },\n { from: 'Ana Costa', subject: 'Lunch next week?', snippet: 'There is a new ramen place near the office, Wednesday works.', time: 'Sat' },\n];\n\nfunction MailRow({ from, service, subject, snippet, time, unread }: (typeof MAIL)[number]) {\n const { colors } = useKit();\n return (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={`${unread ? 'Unread. ' : ''}${from}, ${subject}`}\n style={({ pressed }) => ({ flexDirection: 'row', gap: 12, paddingVertical: 11, paddingLeft: 16, paddingRight: 14, backgroundColor: pressed ? colors.muted : 'transparent' })}\n >\n <Initials name={from} size={40} square={service} />\n <View style={{ flex: 1, minWidth: 0, gap: 1 }}>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>\n <Text numberOfLines={1} style={{ flex: 1, fontSize: 15, color: unread ? colors.foreground : colors.mutedForeground }}>\n {from}\n </Text>\n <Text style={{ fontSize: 12.5, color: unread ? colors.accent : colors.mutedForeground }}>{time}</Text>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>\n <Text numberOfLines={1} style={{ flex: 1, fontSize: 14, color: colors.foreground, opacity: unread ? 1 : 0.8 }}>\n {subject}\n </Text>\n {unread ? <Dot color={colors.accent} size={8} /> : null}\n </View>\n <Text numberOfLines={1} style={{ fontSize: 13.5, color: colors.mutedForeground }}>\n {snippet}\n </Text>\n </View>\n </Pressable>\n );\n}\n\nexport default function AppNavigation({ screen, bottomInset }: DemoProps) {\n const { colors, radius, dark } = useKit();\n const [page, setPage] = useState('Inbox');\n const nav: [DrawerIconName, string, number?][] = [\n ['inbox', 'Inbox', 12],\n ['star', 'Starred'],\n ['send', 'Sent'],\n ['file', 'Drafts', 2],\n ['archive', 'Archive'],\n ['trash', 'Trash'],\n ];\n const labels: [string, string, number?][] = [\n ['Design reviews', colors.accent, 4],\n ['Receipts', colors.success],\n ['Travel', colors.warning, 1],\n ];\n return (\n <Frame screen={screen}>\n <Drawer\n variant=\"push\"\n width=\"78%\"\n topInset={screen ? undefined : STATUS_H}\n bottomInset={bottomInset}\n header={<DrawerProfile name=\"Maya Patel\" email=\"maya@northwind.app\" status=\"online\" onPress={() => {}} />}\n content={\n <>\n {nav.map(([icon, label, badge]) => (\n <DrawerItem key={label} icon={icon} label={label} badge={badge} active={page === label} onPress={() => setPage(label)} />\n ))}\n <DrawerSection title=\"Labels\" trailing={<IconButton icon=\"plus\" label=\"New label\" color={colors.mutedForeground} />} />\n {labels.map(([label, color, badge]) => (\n <DrawerItem key={label} icon={<Dot color={color} />} label={label} badge={badge} active={page === label} onPress={() => setPage(label)} />\n ))}\n </>\n }\n footer={<DrawerFooter icon=\"cloud\" label=\"Storage\" detail=\"12.4 GB of 15 GB used\" progress={0.83} />}\n >\n <Phone>\n <TopBar\n title={page}\n subtitle={page === 'Inbox' ? '3 unread · maya@northwind.app' : undefined}\n right={\n <>\n <IconButton icon=\"search\" label=\"Search mail\" />\n <View style={{ paddingRight: 6 }}>\n <Initials name=\"Maya Patel\" size={32} />\n </View>\n </>\n }\n />\n <Text style={{ paddingHorizontal: 16, paddingTop: 4, paddingBottom: 2, fontSize: 13, color: colors.mutedForeground }}>Today</Text>\n {MAIL.map((m) => (\n <MailRow key={m.subject} {...m} />\n ))}\n <View\n style={{\n position: 'absolute',\n right: 16,\n bottom: 18 + (bottomInset ?? 0),\n height: 52,\n paddingHorizontal: 18,\n flexDirection: 'row',\n alignItems: 'center',\n gap: 8,\n borderRadius: radius.scale(18),\n backgroundColor: colors.accent,\n shadowColor: dark ? colors.background : colors.foreground,\n shadowOpacity: 0.18,\n shadowRadius: 14,\n shadowOffset: { width: 0, height: 6 },\n elevation: 6,\n }}\n >\n <DrawerIcon name=\"plus\" size={20} color={colors.accentForeground} strokeWidth={2} />\n <Text style={{ fontSize: 15, color: colors.accentForeground }}>Compose</Text>\n </View>\n </Phone>\n </Drawer>\n </Frame>\n );\n}\n\n/* Workspace ------------------------------------------------------------------------------------- */\n"
|
|
181
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type ViewProps } from 'react-native';\nimport {\n Drawer,\n DrawerFooter,\n DrawerHeader,\n DrawerIcon,\n DrawerItem,\n DrawerProfile,\n DrawerSection,\n useDrawer,\n type DrawerIconName,\n} from '@/components/ui/drawer';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\n/* Shared phone chrome --------------------------------------------------------------------------- */\n\nconst STATUS_H = 40;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\n/** True when the demo fills a real phone screen: the frame and the fake status bar step aside. */\nconst ScreenMode = React.createContext(false);\n\n/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, screen = false, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <ScreenMode.Provider value={screen}>\n <View\n style={[\n { backgroundColor: colors.background, overflow: 'hidden' },\n screen ? { flex: 1 } : { height: 600, borderRadius: radius.scale(28), borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border },\n style,\n ]}\n {...rest}\n />\n </ScreenMode.Provider>\n );\n}\n\n/** Fake status bar: time, signal and battery, drawn with Views. */\nfunction StatusBar() {\n const { colors, radius } = useKit();\n const ink = colors.foreground;\n return (\n <View style={{ height: STATUS_H, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 15, color: ink, fontVariant: ['tabular-nums'] }}>9:41</Text>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2 }}>\n {[4, 6, 8, 10].map((h) => (\n <View key={h} style={{ width: 3, height: h, borderRadius: radius.capsule(3), backgroundColor: ink }} />\n ))}\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>\n <View style={{ width: 22, height: 11, padding: 1.5, borderRadius: radius.scale(3.5), borderWidth: 1, borderColor: alpha(ink, 0.4) }}>\n <View style={{ width: '72%', flex: 1, borderRadius: radius.scale(2), backgroundColor: ink }} />\n </View>\n <View style={{ width: 1.5, height: 4, borderRadius: 1, backgroundColor: alpha(ink, 0.4) }} />\n </View>\n </View>\n </View>\n );\n}\n\n/** The app screen behind the drawer: status bar plus your content. */\nfunction Phone({ children }: { children: React.ReactNode }) {\n const { colors } = useKit();\n const screen = React.useContext(ScreenMode);\n return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n {screen ? null : <StatusBar />}\n {children}\n </View>\n );\n}\n\nfunction IconButton({ icon, label, onPress, color }: { icon: DrawerIconName; label: string; onPress?: () => void; color?: string }) {\n const { colors, radius } = useKit();\n return (\n <Pressable\n onPress={onPress}\n accessibilityRole=\"button\"\n accessibilityLabel={label}\n style={({ pressed }) => ({\n width: 44,\n height: 44,\n borderRadius: radius.capsule(44),\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: pressed ? colors.muted : 'transparent',\n })}\n >\n <DrawerIcon name={icon} size={22} color={color ?? colors.foreground} />\n </Pressable>\n );\n}\n\n/** The hamburger: toggles the surrounding Drawer through useDrawer(). */\nfunction MenuButton() {\n const { open, setOpen } = useDrawer();\n return <IconButton icon=\"menu\" label=\"Open navigation\" onPress={() => setOpen(!open)} />;\n}\n\n/** A plain inline top bar: hamburger, title (+ subtitle) and trailing actions. */\nfunction TopBar({ title, subtitle, right, menu = true }: { title: string; subtitle?: string; right?: React.ReactNode; menu?: boolean }) {\n const { colors } = useKit();\n return (\n <View style={{ minHeight: 56, flexDirection: 'row', alignItems: 'center', paddingLeft: menu ? 6 : 16, paddingRight: 8, gap: 4 }}>\n {menu ? <MenuButton /> : null}\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: subtitle ? 19 : 22, letterSpacing: -0.4, color: colors.foreground }}>\n {title}\n </Text>\n {subtitle ? (\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: 12.5, color: colors.mutedForeground }}>\n {subtitle}\n </Text>\n ) : null}\n </View>\n {right}\n </View>\n );\n}\n\nfunction Initials({ name, size = 32, square, ring }: { name: string; size?: number; square?: boolean; ring?: string }) {\n const { colors, radius } = useKit();\n const letters = name\n .split(/\\s+/)\n .slice(0, 2)\n .map((w) => w[0])\n .join('');\n return (\n <View\n style={{\n width: size,\n height: size,\n borderRadius: square ? radius.scale(size * 0.28) : radius.capsule(size),\n backgroundColor: square ? colors.foreground : alpha(colors.accent, 0.14),\n borderWidth: ring ? 2 : 0,\n borderColor: ring,\n alignItems: 'center',\n justifyContent: 'center',\n }}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: size * 0.38, color: square ? colors.background : colors.accent }}>{letters}</Text>\n </View>\n );\n}\n\nfunction Dot({ color, size = 10 }: { color: string; size?: number }) {\n const { radius } = useKit();\n return <View style={{ width: size, height: size, borderRadius: radius.capsule(size), backgroundColor: color }} />;\n}\n\n/* Mail ------------------------------------------------------------------------------------------ */\n\nconst MAIL = [\n { from: 'Lena Fischer', subject: 'Offsite agenda — final draft', snippet: 'Attached the plan for Thursday. Shout if the 10:00 slot clashes.', time: '9:41', unread: true },\n { from: 'GitHub', service: true, subject: '[northwind/app] PR #482 merged', snippet: 'feat: offline drafts for the editor (#482) was merged into main.', time: '9:12', unread: true },\n { from: 'Omar Haddad', subject: 'Re: Invoice 2041', snippet: 'Paid this morning — the receipt is in the thread below.', time: '8:03', unread: true },\n { from: 'Figma', service: true, subject: 'Priya commented on Checkout v3', snippet: '“Can we try the sheet at 90% height here?”', time: 'Tue' },\n { from: 'Stripe', service: true, subject: 'Your October payout is on its way', snippet: '₹2,48,000 will arrive in your account in 2 business days.', time: 'Mon' },\n { from: 'Jonas Weber', subject: 'Photos from Lisbon', snippet: 'Finally sorted them — 48 shots, the tram ones came out great.', time: 'Sun' },\n { from: 'Ana Costa', subject: 'Lunch next week?', snippet: 'There is a new ramen place near the office, Wednesday works.', time: 'Sat' },\n];\n\nfunction MailRow({ from, service, subject, snippet, time, unread }: (typeof MAIL)[number]) {\n const { colors } = useKit();\n return (\n <Pressable\n accessibilityRole=\"button\"\n accessibilityLabel={`${unread ? 'Unread. ' : ''}${from}, ${subject}`}\n style={({ pressed }) => ({ flexDirection: 'row', gap: 12, paddingVertical: 11, paddingLeft: 16, paddingRight: 14, backgroundColor: pressed ? colors.muted : 'transparent' })}\n >\n <Initials name={from} size={40} square={service} />\n <View style={{ flex: 1, minWidth: 0, gap: 1 }}>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ flex: 1, fontSize: 15, color: unread ? colors.foreground : colors.mutedForeground }}>\n {from}\n </Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12.5, color: unread ? colors.accent : colors.mutedForeground }}>{time}</Text>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ flex: 1, fontSize: 14, color: colors.foreground, opacity: unread ? 1 : 0.8 }}>\n {subject}\n </Text>\n {unread ? <Dot color={colors.accent} size={8} /> : null}\n </View>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: 13.5, color: colors.mutedForeground }}>\n {snippet}\n </Text>\n </View>\n </Pressable>\n );\n}\n\nexport default function AppNavigation({ screen, bottomInset }: DemoProps) {\n const { colors, radius, dark } = useKit();\n const [page, setPage] = useState('Inbox');\n const nav: [DrawerIconName, string, number?][] = [\n ['inbox', 'Inbox', 12],\n ['star', 'Starred'],\n ['send', 'Sent'],\n ['file', 'Drafts', 2],\n ['archive', 'Archive'],\n ['trash', 'Trash'],\n ];\n const labels: [string, string, number?][] = [\n ['Design reviews', colors.accent, 4],\n ['Receipts', colors.success],\n ['Travel', colors.warning, 1],\n ];\n return (\n <Frame screen={screen}>\n <Drawer\n variant=\"push\"\n width=\"78%\"\n topInset={screen ? undefined : STATUS_H}\n bottomInset={bottomInset}\n header={<DrawerProfile name=\"Maya Patel\" email=\"maya@northwind.app\" status=\"online\" onPress={() => {}} />}\n content={\n <>\n {nav.map(([icon, label, badge]) => (\n <DrawerItem key={label} icon={icon} label={label} badge={badge} active={page === label} onPress={() => setPage(label)} />\n ))}\n <DrawerSection title=\"Labels\" trailing={<IconButton icon=\"plus\" label=\"New label\" color={colors.mutedForeground} />} />\n {labels.map(([label, color, badge]) => (\n <DrawerItem key={label} icon={<Dot color={color} />} label={label} badge={badge} active={page === label} onPress={() => setPage(label)} />\n ))}\n </>\n }\n footer={<DrawerFooter icon=\"cloud\" label=\"Storage\" detail=\"12.4 GB of 15 GB used\" progress={0.83} />}\n >\n <Phone>\n <TopBar\n title={page}\n subtitle={page === 'Inbox' ? '3 unread · maya@northwind.app' : undefined}\n right={\n <>\n <IconButton icon=\"search\" label=\"Search mail\" />\n <View style={{ paddingRight: 6 }}>\n <Initials name=\"Maya Patel\" size={32} />\n </View>\n </>\n }\n />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ paddingHorizontal: 16, paddingTop: 4, paddingBottom: 2, fontSize: 13, color: colors.mutedForeground }}>Today</Text>\n {MAIL.map((m) => (\n <MailRow key={m.subject} {...m} />\n ))}\n <View\n style={{\n position: 'absolute',\n right: 16,\n bottom: 18 + (bottomInset ?? 0),\n minHeight: 52,\n paddingHorizontal: 18,\n flexDirection: 'row',\n alignItems: 'center',\n gap: 8,\n borderRadius: radius.scale(18),\n backgroundColor: colors.accent,\n shadowColor: dark ? colors.background : colors.foreground,\n shadowOpacity: 0.18,\n shadowRadius: 14,\n shadowOffset: { width: 0, height: 6 },\n elevation: 6,\n }}\n >\n <DrawerIcon name=\"plus\" size={20} color={colors.accentForeground} strokeWidth={2} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 15, color: colors.accentForeground }}>Compose</Text>\n </View>\n </Phone>\n </Drawer>\n </Frame>\n );\n}\n\n/* Workspace ------------------------------------------------------------------------------------- */\n"
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
184
|
"name": "WorkspaceSwitcher",
|
|
185
185
|
"title": "Workspace Switcher",
|
|
186
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type ViewProps } from 'react-native';\nimport {\n Drawer,\n DrawerFooter,\n DrawerHeader,\n DrawerIcon,\n DrawerItem,\n DrawerProfile,\n DrawerSection,\n useDrawer,\n type DrawerIconName,\n} from '@/components/ui/drawer';\nimport { alpha, useKit } from '@/lib/kit';\n\nconst STATUS_H = 40;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\n/** True when the demo fills a real phone screen: the frame and the fake status bar step aside. */\nconst ScreenMode = React.createContext(false);\n\n/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, screen = false, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <ScreenMode.Provider value={screen}>\n <View\n style={[\n { backgroundColor: colors.background, overflow: 'hidden' },\n screen ? { flex: 1 } : { height: 600, borderRadius: radius.scale(28), borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border },\n style,\n ]}\n {...rest}\n />\n </ScreenMode.Provider>\n );\n}\n\n/** Fake status bar: time, signal and battery, drawn with Views. */\nfunction StatusBar() {\n const { colors, radius } = useKit();\n const ink = colors.foreground;\n return (\n <View style={{ height: STATUS_H, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24 }}>\n <Text style={{ fontSize: 15, color: ink, fontVariant: ['tabular-nums'] }}>9:41</Text>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2 }}>\n {[4, 6, 8, 10].map((h) => (\n <View key={h} style={{ width: 3, height: h, borderRadius: radius.capsule(3), backgroundColor: ink }} />\n ))}\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>\n <View style={{ width: 22, height: 11, padding: 1.5, borderRadius: radius.scale(3.5), borderWidth: 1, borderColor: alpha(ink, 0.4) }}>\n <View style={{ width: '72%', flex: 1, borderRadius: radius.scale(2), backgroundColor: ink }} />\n </View>\n <View style={{ width: 1.5, height: 4, borderRadius: 1, backgroundColor: alpha(ink, 0.4) }} />\n </View>\n </View>\n </View>\n );\n}\n\n/** The app screen behind the drawer: status bar plus your content. */\nfunction Phone({ children }: { children: React.ReactNode }) {\n const { colors } = useKit();\n const screen = React.useContext(ScreenMode);\n return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n {screen ? null : <StatusBar />}\n {children}\n </View>\n );\n}\n\nfunction IconButton({ icon, label, onPress, color }: { icon: DrawerIconName; label: string; onPress?: () => void; color?: string }) {\n const { colors, radius } = useKit();\n return (\n <Pressable\n onPress={onPress}\n accessibilityRole=\"button\"\n accessibilityLabel={label}\n style={({ pressed }) => ({\n width: 44,\n height: 44,\n borderRadius: radius.capsule(44),\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: pressed ? colors.muted : 'transparent',\n })}\n >\n <DrawerIcon name={icon} size={22} color={color ?? colors.foreground} />\n </Pressable>\n );\n}\n\n/** The hamburger: toggles the surrounding Drawer through useDrawer(). */\nfunction MenuButton() {\n const { open, setOpen } = useDrawer();\n return <IconButton icon=\"menu\" label=\"Open navigation\" onPress={() => setOpen(!open)} />;\n}\n\n/** A plain inline top bar: hamburger, title (+ subtitle) and trailing actions. */\nfunction TopBar({ title, subtitle, right, menu = true }: { title: string; subtitle?: string; right?: React.ReactNode; menu?: boolean }) {\n const { colors } = useKit();\n return (\n <View style={{ height: 56, flexDirection: 'row', alignItems: 'center', paddingLeft: menu ? 6 : 16, paddingRight: 8, gap: 4 }}>\n {menu ? <MenuButton /> : null}\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text numberOfLines={1} style={{ fontSize: subtitle ? 19 : 22, letterSpacing: -0.4, color: colors.foreground }}>\n {title}\n </Text>\n {subtitle ? (\n <Text numberOfLines={1} style={{ fontSize: 12.5, color: colors.mutedForeground }}>\n {subtitle}\n </Text>\n ) : null}\n </View>\n {right}\n </View>\n );\n}\n\nfunction Initials({ name, size = 32, square, ring }: { name: string; size?: number; square?: boolean; ring?: string }) {\n const { colors, radius } = useKit();\n const letters = name\n .split(/\\s+/)\n .slice(0, 2)\n .map((w) => w[0])\n .join('');\n return (\n <View\n style={{\n width: size,\n height: size,\n borderRadius: square ? radius.scale(size * 0.28) : radius.capsule(size),\n backgroundColor: square ? colors.foreground : alpha(colors.accent, 0.14),\n borderWidth: ring ? 2 : 0,\n borderColor: ring,\n alignItems: 'center',\n justifyContent: 'center',\n }}\n >\n <Text style={{ fontSize: size * 0.38, color: square ? colors.background : colors.accent }}>{letters}</Text>\n </View>\n );\n}\n\nfunction Dot({ color, size = 10 }: { color: string; size?: number }) {\n const { radius } = useKit();\n return <View style={{ width: size, height: size, borderRadius: radius.capsule(size), backgroundColor: color }} />;\n}\n\n/* Mail ------------------------------------------------------------------------------------------ */\n\nconst WORKSPACES = [\n { name: 'Northwind', detail: 'northwind.app · 24 members' },\n { name: 'Side Quests', detail: 'Personal · 1 member' },\n { name: 'Acme Studio', detail: 'acme.studio · 8 members' },\n];\n\ntype TaskState = 'In progress' | 'In review' | 'Todo';\n\nconst TASKS: { id: string; title: string; state: TaskState; due: string; done: number; total: number; people: string[] }[] = [\n { id: 'MOB-212', title: 'Sheet snap points jitter on Android 14', state: 'In progress', due: 'Today', done: 3, total: 5, people: ['Maya Patel', 'Omar Haddad'] },\n { id: 'MOB-208', title: 'Offline drafts for the editor', state: 'In review', due: 'Thu', done: 6, total: 6, people: ['Lena Fischer'] },\n { id: 'MOB-199', title: 'Rewrite the push permission prompt', state: 'Todo', due: 'Mon', done: 0, total: 3, people: ['Priya Nair', 'Maya Patel'] },\n { id: 'MOB-187', title: 'Haptics on tab change', state: 'Todo', due: 'Oct 28', done: 1, total: 2, people: ['Jonas Weber'] },\n];\n\nfunction StatePill({ state }: { state: TaskState }) {\n const { colors, radius } = useKit();\n const tone = state === 'In progress' ? colors.accent : state === 'In review' ? colors.warning : colors.mutedForeground;\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, height: 24, paddingHorizontal: 9, borderRadius: radius.capsule(24), backgroundColor: alpha(tone, 0.13) }}>\n <Dot color={tone} size={7} />\n <Text style={{ fontSize: 12, color: colors.foreground }}>{state}</Text>\n </View>\n );\n}\n\nfunction TaskCard({ id, title, state, due, done, total, people }: (typeof TASKS)[number]) {\n const { colors, radius, dark } = useKit();\n return (\n <View\n style={{\n padding: 14,\n gap: 10,\n borderRadius: radius.scale(16),\n backgroundColor: colors.card,\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: colors.border,\n shadowColor: dark ? colors.background : colors.foreground,\n shadowOpacity: dark ? 0.4 : 0.05,\n shadowRadius: 10,\n shadowOffset: { width: 0, height: 3 },\n elevation: 1,\n }}\n >\n <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>\n <StatePill state={state} />\n <Text style={{ fontSize: 12, color: colors.mutedForeground, fontVariant: ['tabular-nums'] }}>{id}</Text>\n </View>\n <Text numberOfLines={1} style={{ fontSize: 15.5, letterSpacing: -0.2, color: colors.foreground }}>\n {title}\n </Text>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 14 }}>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 5 }}>\n <DrawerIcon name=\"calendar\" size={14} color={due === 'Today' ? colors.danger : colors.mutedForeground} />\n <Text style={{ fontSize: 12.5, color: due === 'Today' ? colors.danger : colors.mutedForeground }}>{due}</Text>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 5 }}>\n <DrawerIcon name=\"check\" size={14} color={colors.mutedForeground} />\n <Text style={{ fontSize: 12.5, color: colors.mutedForeground, fontVariant: ['tabular-nums'] }}>\n {done}/{total}\n </Text>\n </View>\n <View style={{ flex: 1 }} />\n <View style={{ flexDirection: 'row' }}>\n {people.map((p, i) => (\n <View key={p} style={{ marginLeft: i ? -8 : 0 }}>\n <Initials name={p} size={26} ring={colors.card} />\n </View>\n ))}\n </View>\n </View>\n </View>\n );\n}\n\nexport default function WorkspaceSwitcher({ screen, bottomInset }: DemoProps) {\n const { colors, radius } = useKit();\n const [workspace, setWorkspace] = useState(WORKSPACES[0]);\n const [picking, setPicking] = useState(false);\n const [page, setPage] = useState('My issues');\n const [filter, setFilter] = useState('Active');\n return (\n <Frame screen={screen}>\n <Drawer\n topInset={screen ? undefined : STATUS_H}\n bottomInset={bottomInset}\n header={<DrawerProfile name={workspace.name} email={workspace.detail} onPress={() => setPicking((v) => !v)} accessibilityHint=\"Switch workspace\" />}\n content={\n picking ? (\n <>\n <DrawerSection title=\"Workspaces\" divider={false} />\n {WORKSPACES.map((w) => (\n <DrawerItem\n key={w.name}\n label={w.name}\n icon={w.name === workspace.name ? 'check' : 'layers'}\n active={w.name === workspace.name}\n closeOnPress={false}\n onPress={() => {\n setWorkspace(w);\n setPicking(false);\n }}\n />\n ))}\n <DrawerItem icon=\"plus\" label=\"Create workspace\" closeOnPress={false} />\n </>\n ) : (\n <>\n {(\n [\n ['inbox', 'Inbox', 5, 'accent'],\n ['target', 'My issues', 14],\n ['search', 'Search'],\n ] as [DrawerIconName, string, number?, 'accent'?][]\n ).map(([icon, label, badge, badgeTone]) => (\n <DrawerItem key={label} icon={icon} label={label} badge={badge} badgeTone={badgeTone} active={page === label} onPress={() => setPage(label)} />\n ))}\n <DrawerSection title=\"Teams\" trailing={<IconButton icon=\"plus\" label=\"New team\" color={colors.mutedForeground} />} />\n {(\n [\n ['layers', 'Mobile', 8],\n ['layers', 'Platform', 3],\n ['users', 'Design'],\n ] as [DrawerIconName, string, number?][]\n ).map(([icon, label, badge]) => (\n <DrawerItem key={label} icon={icon} label={label} badge={badge} active={page === label} onPress={() => setPage(label)} />\n ))}\n </>\n )\n }\n footer={\n <>\n <DrawerItem icon=\"settings\" label=\"Settings\" active={page === 'Settings'} onPress={() => setPage('Settings')} />\n <DrawerItem icon=\"logout\" label=\"Log out\" tone=\"danger\" />\n </>\n }\n >\n <Phone>\n <TopBar title={page} subtitle={`${workspace.name} · Mobile team`} right={<IconButton icon=\"plus\" label=\"New issue\" />} />\n <View style={{ flexDirection: 'row', gap: 8, paddingHorizontal: 16, paddingTop: 2, paddingBottom: 12 }}>\n {[\n ['Active', '4'],\n ['Backlog', '9'],\n ['Done', '23'],\n ].map(([label, n]) => {\n const on = filter === label;\n return (\n <Pressable\n key={label}\n onPress={() => setFilter(label)}\n accessibilityRole=\"button\"\n accessibilityState={{ selected: on }}\n style={{\n height: 32,\n paddingHorizontal: 12,\n flexDirection: 'row',\n alignItems: 'center',\n gap: 6,\n borderRadius: radius.capsule(32),\n backgroundColor: on ? alpha(colors.accent, 0.13) : 'transparent',\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: on ? alpha(colors.accent, 0.3) : colors.border,\n }}\n >\n <Text style={{ fontSize: 13.5, color: on ? colors.accent : colors.foreground }}>{label}</Text>\n <Text style={{ fontSize: 12.5, color: on ? colors.accent : colors.mutedForeground }}>{n}</Text>\n </Pressable>\n );\n })}\n </View>\n <View style={{ paddingHorizontal: 16, gap: 10 }}>\n {TASKS.map((t) => (\n <TaskCard key={t.id} {...t} />\n ))}\n </View>\n </Phone>\n </Drawer>\n </Frame>\n );\n}\n\n/* Stays (right panel) --------------------------------------------------------------------------- */\n"
|
|
186
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type ViewProps } from 'react-native';\nimport {\n Drawer,\n DrawerFooter,\n DrawerHeader,\n DrawerIcon,\n DrawerItem,\n DrawerProfile,\n DrawerSection,\n useDrawer,\n type DrawerIconName,\n} from '@/components/ui/drawer';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\n/* Shared phone chrome --------------------------------------------------------------------------- */\n\nconst STATUS_H = 40;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\n/** True when the demo fills a real phone screen: the frame and the fake status bar step aside. */\nconst ScreenMode = React.createContext(false);\n\n/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, screen = false, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <ScreenMode.Provider value={screen}>\n <View\n style={[\n { backgroundColor: colors.background, overflow: 'hidden' },\n screen ? { flex: 1 } : { height: 600, borderRadius: radius.scale(28), borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border },\n style,\n ]}\n {...rest}\n />\n </ScreenMode.Provider>\n );\n}\n\n/** Fake status bar: time, signal and battery, drawn with Views. */\nfunction StatusBar() {\n const { colors, radius } = useKit();\n const ink = colors.foreground;\n return (\n <View style={{ height: STATUS_H, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 15, color: ink, fontVariant: ['tabular-nums'] }}>9:41</Text>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2 }}>\n {[4, 6, 8, 10].map((h) => (\n <View key={h} style={{ width: 3, height: h, borderRadius: radius.capsule(3), backgroundColor: ink }} />\n ))}\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>\n <View style={{ width: 22, height: 11, padding: 1.5, borderRadius: radius.scale(3.5), borderWidth: 1, borderColor: alpha(ink, 0.4) }}>\n <View style={{ width: '72%', flex: 1, borderRadius: radius.scale(2), backgroundColor: ink }} />\n </View>\n <View style={{ width: 1.5, height: 4, borderRadius: 1, backgroundColor: alpha(ink, 0.4) }} />\n </View>\n </View>\n </View>\n );\n}\n\n/** The app screen behind the drawer: status bar plus your content. */\nfunction Phone({ children }: { children: React.ReactNode }) {\n const { colors } = useKit();\n const screen = React.useContext(ScreenMode);\n return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n {screen ? null : <StatusBar />}\n {children}\n </View>\n );\n}\n\nfunction IconButton({ icon, label, onPress, color }: { icon: DrawerIconName; label: string; onPress?: () => void; color?: string }) {\n const { colors, radius } = useKit();\n return (\n <Pressable\n onPress={onPress}\n accessibilityRole=\"button\"\n accessibilityLabel={label}\n style={({ pressed }) => ({\n width: 44,\n height: 44,\n borderRadius: radius.capsule(44),\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: pressed ? colors.muted : 'transparent',\n })}\n >\n <DrawerIcon name={icon} size={22} color={color ?? colors.foreground} />\n </Pressable>\n );\n}\n\n/** The hamburger: toggles the surrounding Drawer through useDrawer(). */\nfunction MenuButton() {\n const { open, setOpen } = useDrawer();\n return <IconButton icon=\"menu\" label=\"Open navigation\" onPress={() => setOpen(!open)} />;\n}\n\n/** A plain inline top bar: hamburger, title (+ subtitle) and trailing actions. */\nfunction TopBar({ title, subtitle, right, menu = true }: { title: string; subtitle?: string; right?: React.ReactNode; menu?: boolean }) {\n const { colors } = useKit();\n return (\n <View style={{ minHeight: 56, flexDirection: 'row', alignItems: 'center', paddingLeft: menu ? 6 : 16, paddingRight: 8, gap: 4 }}>\n {menu ? <MenuButton /> : null}\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: subtitle ? 19 : 22, letterSpacing: -0.4, color: colors.foreground }}>\n {title}\n </Text>\n {subtitle ? (\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: 12.5, color: colors.mutedForeground }}>\n {subtitle}\n </Text>\n ) : null}\n </View>\n {right}\n </View>\n );\n}\n\nfunction Initials({ name, size = 32, square, ring }: { name: string; size?: number; square?: boolean; ring?: string }) {\n const { colors, radius } = useKit();\n const letters = name\n .split(/\\s+/)\n .slice(0, 2)\n .map((w) => w[0])\n .join('');\n return (\n <View\n style={{\n width: size,\n height: size,\n borderRadius: square ? radius.scale(size * 0.28) : radius.capsule(size),\n backgroundColor: square ? colors.foreground : alpha(colors.accent, 0.14),\n borderWidth: ring ? 2 : 0,\n borderColor: ring,\n alignItems: 'center',\n justifyContent: 'center',\n }}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: size * 0.38, color: square ? colors.background : colors.accent }}>{letters}</Text>\n </View>\n );\n}\n\nfunction Dot({ color, size = 10 }: { color: string; size?: number }) {\n const { radius } = useKit();\n return <View style={{ width: size, height: size, borderRadius: radius.capsule(size), backgroundColor: color }} />;\n}\n\n/* Mail ------------------------------------------------------------------------------------------ */\n\nconst WORKSPACES = [\n { name: 'Northwind', detail: 'northwind.app · 24 members' },\n { name: 'Side Quests', detail: 'Personal · 1 member' },\n { name: 'Acme Studio', detail: 'acme.studio · 8 members' },\n];\n\ntype TaskState = 'In progress' | 'In review' | 'Todo';\n\nconst TASKS: { id: string; title: string; state: TaskState; due: string; done: number; total: number; people: string[] }[] = [\n { id: 'MOB-212', title: 'Sheet snap points jitter on Android 14', state: 'In progress', due: 'Today', done: 3, total: 5, people: ['Maya Patel', 'Omar Haddad'] },\n { id: 'MOB-208', title: 'Offline drafts for the editor', state: 'In review', due: 'Thu', done: 6, total: 6, people: ['Lena Fischer'] },\n { id: 'MOB-199', title: 'Rewrite the push permission prompt', state: 'Todo', due: 'Mon', done: 0, total: 3, people: ['Priya Nair', 'Maya Patel'] },\n { id: 'MOB-187', title: 'Haptics on tab change', state: 'Todo', due: 'Oct 28', done: 1, total: 2, people: ['Jonas Weber'] },\n];\n\nfunction StatePill({ state }: { state: TaskState }) {\n const { colors, radius } = useKit();\n const tone = state === 'In progress' ? colors.accent : state === 'In review' ? colors.warning : colors.mutedForeground;\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, minHeight: 24, paddingHorizontal: 9, borderRadius: radius.capsule(24), backgroundColor: alpha(tone, 0.13) }}>\n <Dot color={tone} size={7} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12, color: colors.foreground }}>{state}</Text>\n </View>\n );\n}\n\nfunction TaskCard({ id, title, state, due, done, total, people }: (typeof TASKS)[number]) {\n const { colors, radius, dark } = useKit();\n return (\n <View\n style={{\n padding: 14,\n gap: 10,\n borderRadius: radius.scale(16),\n backgroundColor: colors.card,\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: colors.border,\n shadowColor: dark ? colors.background : colors.foreground,\n shadowOpacity: dark ? 0.4 : 0.05,\n shadowRadius: 10,\n shadowOffset: { width: 0, height: 3 },\n elevation: 1,\n }}\n >\n <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>\n <StatePill state={state} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12, color: colors.mutedForeground, fontVariant: ['tabular-nums'] }}>{id}</Text>\n </View>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: 15.5, letterSpacing: -0.2, color: colors.foreground }}>\n {title}\n </Text>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 14 }}>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 5 }}>\n <DrawerIcon name=\"calendar\" size={14} color={due === 'Today' ? colors.danger : colors.mutedForeground} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12.5, color: due === 'Today' ? colors.danger : colors.mutedForeground }}>{due}</Text>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 5 }}>\n <DrawerIcon name=\"check\" size={14} color={colors.mutedForeground} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12.5, color: colors.mutedForeground, fontVariant: ['tabular-nums'] }}>\n {done}/{total}\n </Text>\n </View>\n <View style={{ flex: 1 }} />\n <View style={{ flexDirection: 'row' }}>\n {people.map((p, i) => (\n <View key={p} style={{ marginLeft: i ? -8 : 0 }}>\n <Initials name={p} size={26} ring={colors.card} />\n </View>\n ))}\n </View>\n </View>\n </View>\n );\n}\n\nexport default function WorkspaceSwitcher({ screen, bottomInset }: DemoProps) {\n const { colors, radius } = useKit();\n const [workspace, setWorkspace] = useState(WORKSPACES[0]);\n const [picking, setPicking] = useState(false);\n const [page, setPage] = useState('My issues');\n const [filter, setFilter] = useState('Active');\n return (\n <Frame screen={screen}>\n <Drawer\n topInset={screen ? undefined : STATUS_H}\n bottomInset={bottomInset}\n header={<DrawerProfile name={workspace.name} email={workspace.detail} onPress={() => setPicking((v) => !v)} accessibilityHint=\"Switch workspace\" />}\n content={\n picking ? (\n <>\n <DrawerSection title=\"Workspaces\" divider={false} />\n {WORKSPACES.map((w) => (\n <DrawerItem\n key={w.name}\n label={w.name}\n icon={w.name === workspace.name ? 'check' : 'layers'}\n active={w.name === workspace.name}\n closeOnPress={false}\n onPress={() => {\n setWorkspace(w);\n setPicking(false);\n }}\n />\n ))}\n <DrawerItem icon=\"plus\" label=\"Create workspace\" closeOnPress={false} />\n </>\n ) : (\n <>\n {(\n [\n ['inbox', 'Inbox', 5, 'accent'],\n ['target', 'My issues', 14],\n ['search', 'Search'],\n ] as [DrawerIconName, string, number?, 'accent'?][]\n ).map(([icon, label, badge, badgeTone]) => (\n <DrawerItem key={label} icon={icon} label={label} badge={badge} badgeTone={badgeTone} active={page === label} onPress={() => setPage(label)} />\n ))}\n <DrawerSection title=\"Teams\" trailing={<IconButton icon=\"plus\" label=\"New team\" color={colors.mutedForeground} />} />\n {(\n [\n ['layers', 'Mobile', 8],\n ['layers', 'Platform', 3],\n ['users', 'Design'],\n ] as [DrawerIconName, string, number?][]\n ).map(([icon, label, badge]) => (\n <DrawerItem key={label} icon={icon} label={label} badge={badge} active={page === label} onPress={() => setPage(label)} />\n ))}\n </>\n )\n }\n footer={\n <>\n <DrawerItem icon=\"settings\" label=\"Settings\" active={page === 'Settings'} onPress={() => setPage('Settings')} />\n <DrawerItem icon=\"logout\" label=\"Log out\" tone=\"danger\" />\n </>\n }\n >\n <Phone>\n <TopBar title={page} subtitle={`${workspace.name} · Mobile team`} right={<IconButton icon=\"plus\" label=\"New issue\" />} />\n <View style={{ flexDirection: 'row', gap: 8, paddingHorizontal: 16, paddingTop: 2, paddingBottom: 12 }}>\n {[\n ['Active', '4'],\n ['Backlog', '9'],\n ['Done', '23'],\n ].map(([label, n]) => {\n const on = filter === label;\n return (\n <Pressable\n key={label}\n onPress={() => setFilter(label)}\n accessibilityRole=\"button\"\n accessibilityState={{ selected: on }}\n style={{\n minHeight: 32,\n paddingHorizontal: 12,\n flexDirection: 'row',\n alignItems: 'center',\n gap: 6,\n borderRadius: radius.capsule(32),\n backgroundColor: on ? alpha(colors.accent, 0.13) : 'transparent',\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: on ? alpha(colors.accent, 0.3) : colors.border,\n }}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 13.5, color: on ? colors.accent : colors.foreground }}>{label}</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12.5, color: on ? colors.accent : colors.mutedForeground }}>{n}</Text>\n </Pressable>\n );\n })}\n </View>\n <View style={{ paddingHorizontal: 16, gap: 10 }}>\n {TASKS.map((t) => (\n <TaskCard key={t.id} {...t} />\n ))}\n </View>\n </Phone>\n </Drawer>\n </Frame>\n );\n}\n\n/* Stays (right panel) --------------------------------------------------------------------------- */\n"
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"name": "RightPanel",
|
|
190
190
|
"title": "Right Panel",
|
|
191
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type ViewProps } from 'react-native';\nimport { Button } from '@/components/ui/button';\nimport {\n Drawer,\n DrawerFooter,\n DrawerHeader,\n DrawerIcon,\n DrawerItem,\n DrawerProfile,\n DrawerSection,\n useDrawer,\n type DrawerIconName,\n} from '@/components/ui/drawer';\nimport { alpha, useKit } from '@/lib/kit';\n\nconst STATUS_H = 40;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\n/** True when the demo fills a real phone screen: the frame and the fake status bar step aside. */\nconst ScreenMode = React.createContext(false);\n\n/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, screen = false, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <ScreenMode.Provider value={screen}>\n <View\n style={[\n { backgroundColor: colors.background, overflow: 'hidden' },\n screen ? { flex: 1 } : { height: 600, borderRadius: radius.scale(28), borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border },\n style,\n ]}\n {...rest}\n />\n </ScreenMode.Provider>\n );\n}\n\n/** Fake status bar: time, signal and battery, drawn with Views. */\nfunction StatusBar() {\n const { colors, radius } = useKit();\n const ink = colors.foreground;\n return (\n <View style={{ height: STATUS_H, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24 }}>\n <Text style={{ fontSize: 15, color: ink, fontVariant: ['tabular-nums'] }}>9:41</Text>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2 }}>\n {[4, 6, 8, 10].map((h) => (\n <View key={h} style={{ width: 3, height: h, borderRadius: radius.capsule(3), backgroundColor: ink }} />\n ))}\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>\n <View style={{ width: 22, height: 11, padding: 1.5, borderRadius: radius.scale(3.5), borderWidth: 1, borderColor: alpha(ink, 0.4) }}>\n <View style={{ width: '72%', flex: 1, borderRadius: radius.scale(2), backgroundColor: ink }} />\n </View>\n <View style={{ width: 1.5, height: 4, borderRadius: 1, backgroundColor: alpha(ink, 0.4) }} />\n </View>\n </View>\n </View>\n );\n}\n\n/** The app screen behind the drawer: status bar plus your content. */\nfunction Phone({ children }: { children: React.ReactNode }) {\n const { colors } = useKit();\n const screen = React.useContext(ScreenMode);\n return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n {screen ? null : <StatusBar />}\n {children}\n </View>\n );\n}\n\nfunction IconButton({ icon, label, onPress, color }: { icon: DrawerIconName; label: string; onPress?: () => void; color?: string }) {\n const { colors, radius } = useKit();\n return (\n <Pressable\n onPress={onPress}\n accessibilityRole=\"button\"\n accessibilityLabel={label}\n style={({ pressed }) => ({\n width: 44,\n height: 44,\n borderRadius: radius.capsule(44),\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: pressed ? colors.muted : 'transparent',\n })}\n >\n <DrawerIcon name={icon} size={22} color={color ?? colors.foreground} />\n </Pressable>\n );\n}\n\n/** The hamburger: toggles the surrounding Drawer through useDrawer(). */\nfunction MenuButton() {\n const { open, setOpen } = useDrawer();\n return <IconButton icon=\"menu\" label=\"Open navigation\" onPress={() => setOpen(!open)} />;\n}\n\n/** A plain inline top bar: hamburger, title (+ subtitle) and trailing actions. */\nfunction TopBar({ title, subtitle, right, menu = true }: { title: string; subtitle?: string; right?: React.ReactNode; menu?: boolean }) {\n const { colors } = useKit();\n return (\n <View style={{ height: 56, flexDirection: 'row', alignItems: 'center', paddingLeft: menu ? 6 : 16, paddingRight: 8, gap: 4 }}>\n {menu ? <MenuButton /> : null}\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text numberOfLines={1} style={{ fontSize: subtitle ? 19 : 22, letterSpacing: -0.4, color: colors.foreground }}>\n {title}\n </Text>\n {subtitle ? (\n <Text numberOfLines={1} style={{ fontSize: 12.5, color: colors.mutedForeground }}>\n {subtitle}\n </Text>\n ) : null}\n </View>\n {right}\n </View>\n );\n}\n\ntype ArtKind = 'sunset' | 'grid' | 'rings' | 'stripes' | 'split';\n\n/**\n * Album art drawn with plain Views in theme colours. `size` 0 stretches to the parent width\n * (then pass `height`).\n */\nfunction Art({ kind, size, height, radiusSize }: { kind: ArtKind; size: number; height?: number; radiusSize?: number }) {\n const { colors, radius } = useKit();\n const h = height ?? size;\n const w = size || undefined;\n const d = Math.min(size || h, h);\n const box = {\n width: w,\n alignSelf: size ? undefined : ('stretch' as const),\n height: h,\n borderRadius: radiusSize ?? radius.scale(size > 60 ? 14 : 8),\n overflow: 'hidden' as const,\n };\n const on = colors.accentForeground;\n if (kind === 'sunset') {\n return (\n <View style={[box, { backgroundColor: colors.accent, alignItems: 'center' }]}>\n <View style={{ position: 'absolute', top: h * 0.3, width: d * 0.5, height: d * 0.5, borderRadius: radius.capsule(d), backgroundColor: alpha(on, 0.85) }} />\n <View style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: h * 0.34, backgroundColor: alpha(colors.foreground, 0.35) }} />\n <View style={{ position: 'absolute', left: 0, right: 0, bottom: h * 0.22, height: Math.max(1, h * 0.03), backgroundColor: alpha(on, 0.35) }} />\n </View>\n );\n }\n if (kind === 'grid') {\n const cell = d * 0.18;\n return (\n <View style={[box, { backgroundColor: colors.foreground, alignItems: 'center', justifyContent: 'center' }]}>\n <View style={{ width: cell * 3 + cell * 0.5 * 2, flexDirection: 'row', flexWrap: 'wrap', gap: cell * 0.5 }}>\n {[1, 0.25, 0.6, 0.25, 1, 0.25, 0.6, 0.25, 1].map((a, i) => (\n <View key={i} style={{ width: cell, height: cell, borderRadius: radius.scale(cell * 0.3), backgroundColor: alpha(colors.accent, a) }} />\n ))}\n </View>\n </View>\n );\n }\n if (kind === 'rings') {\n return (\n <View style={[box, { backgroundColor: alpha(colors.accent, 0.16), alignItems: 'center', justifyContent: 'center' }]}>\n {[0.95, 0.7, 0.45, 0.2].map((f) => (\n <View\n key={f}\n style={{\n position: 'absolute',\n width: d * f,\n height: d * f,\n borderRadius: radius.capsule(d),\n borderWidth: Math.max(1.5, d * 0.03),\n borderColor: alpha(colors.accent, 1 - f * 0.6),\n }}\n />\n ))}\n </View>\n );\n }\n if (kind === 'stripes') {\n return (\n <View style={[box, { backgroundColor: colors.muted, justifyContent: 'center', alignItems: 'center' }]}>\n <View style={{ width: d * 1.6, gap: d * 0.07, transform: [{ rotate: '-30deg' }] }}>\n {[0.9, 0.55, 0.3, 0.55, 0.9].map((a, i) => (\n <View key={i} style={{ height: d * 0.1, backgroundColor: alpha(colors.accent, a) }} />\n ))}\n </View>\n </View>\n );\n }\n return (\n <View style={[box, { backgroundColor: colors.foreground, flexDirection: 'row' }]}>\n <View style={{ flex: 1, backgroundColor: colors.accent }} />\n <View style={{ flex: 1 }} />\n <View\n style={{\n position: 'absolute',\n left: (w ?? d) / 2 - d * 0.22,\n top: h / 2 - d * 0.22,\n width: d * 0.44,\n height: d * 0.44,\n borderRadius: radius.capsule(d),\n backgroundColor: colors.background,\n }}\n />\n </View>\n );\n}\n\nexport default function RightPanel({ screen, bottomInset }: DemoProps) {\n const { colors, radius } = useKit();\n const [open, setOpen] = useState(false);\n const [price, setPrice] = useState('Any price');\n const [tags, setTags] = useState<string[]>(['Free cancellation']);\n const toggle = (t: string) => setTags((s) => (s.includes(t) ? s.filter((x) => x !== t) : [...s, t]));\n const count = 48 - tags.length * 9 - (price === 'Any price' ? 0 : 11);\n const active = tags.length + (price !== 'Any price' ? 1 : 0);\n return (\n <Frame screen={screen}>\n <Drawer\n open={open}\n onOpenChange={setOpen}\n side=\"right\"\n width=\"86%\"\n topInset={screen ? undefined : STATUS_H}\n bottomInset={bottomInset}\n accessibilityLabel=\"Filters\"\n header={<DrawerHeader title=\"Filters\" subtitle=\"Porto · Jun 3 – 7 · 2 guests\" showClose onClose={() => setOpen(false)} />}\n content={\n <>\n <DrawerSection title=\"Price per night\" divider={false} />\n {['Any price', 'Under ₹5,000', '₹5,000 – ₹10,000', '₹10,000+'].map((p) => (\n <DrawerItem key={p} label={p} icon={price === p ? 'check' : <View />} active={price === p} closeOnPress={false} onPress={() => setPrice(p)} />\n ))}\n <DrawerSection title=\"Amenities\" />\n {['Free cancellation', 'Breakfast included', 'Pet friendly', 'Workspace'].map((t) => (\n <DrawerItem key={t} label={t} icon={tags.includes(t) ? 'check' : <View />} active={tags.includes(t)} closeOnPress={false} onPress={() => toggle(t)} />\n ))}\n </>\n }\n footer={\n <View style={{ flexDirection: 'row', gap: 8, paddingHorizontal: 4 }}>\n <Button\n variant=\"outline\"\n style={{ flex: 1 }}\n onPress={() => {\n setPrice('Any price');\n setTags([]);\n }}\n >\n Reset\n </Button>\n <Button variant=\"accent\" style={{ flex: 1.4 }} onPress={() => setOpen(false)}>\n {`Show ${count} stays`}\n </Button>\n </View>\n }\n >\n <Phone>\n <TopBar\n menu={false}\n title=\"Stays in Porto\"\n subtitle={`${count} places · Jun 3 – 7`}\n right={\n <Button size=\"sm\" variant=\"secondary\" pill left={<DrawerIcon name=\"filter\" size={16} />} onPress={() => setOpen(true)}>\n {active ? `Filters · ${active}` : 'Filters'}\n </Button>\n }\n />\n <View style={{ paddingHorizontal: 16, paddingTop: 4, gap: 16 }}>\n {[\n { name: 'Casa da Ribeira', meta: 'River view · 1 bedroom', price: '₹6,800', rating: '4.92', art: 'sunset' as const },\n { name: 'Azulejo Loft', meta: 'Breakfast included', price: '₹4,800', rating: '4.81', art: 'grid' as const },\n { name: 'Miradouro Suites', meta: 'Free cancellation', price: '₹12,500', rating: '4.97', art: 'rings' as const },\n ].map((s) => (\n <View key={s.name} style={{ gap: 8 }}>\n <Art kind={s.art} size={0} height={112} radiusSize={radius.scale(18)} />\n <View style={{ flexDirection: 'row', alignItems: 'flex-start', gap: 8 }}>\n <View style={{ flex: 1 }}>\n <Text style={{ fontSize: 15, color: colors.foreground }}>{s.name}</Text>\n <Text style={{ fontSize: 13, color: colors.mutedForeground }}>{s.meta}</Text>\n </View>\n <View style={{ alignItems: 'flex-end' }}>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>\n <DrawerIcon name=\"star\" size={13} color={colors.foreground} />\n <Text style={{ fontSize: 13, color: colors.foreground }}>{s.rating}</Text>\n </View>\n <Text style={{ fontSize: 13, color: colors.mutedForeground }}>\n <Text style={{ color: colors.foreground }}>{s.price}</Text> night\n </Text>\n </View>\n </View>\n </View>\n ))}\n </View>\n </Phone>\n </Drawer>\n </Frame>\n );\n}\n\n/* Music ----------------------------------------------------------------------------------------- */\n"
|
|
191
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type ViewProps } from 'react-native';\nimport { Button } from '@/components/ui/button';\nimport {\n Drawer,\n DrawerFooter,\n DrawerHeader,\n DrawerIcon,\n DrawerItem,\n DrawerProfile,\n DrawerSection,\n useDrawer,\n type DrawerIconName,\n} from '@/components/ui/drawer';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\n/* Shared phone chrome --------------------------------------------------------------------------- */\n\nconst STATUS_H = 40;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\n/** True when the demo fills a real phone screen: the frame and the fake status bar step aside. */\nconst ScreenMode = React.createContext(false);\n\n/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, screen = false, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <ScreenMode.Provider value={screen}>\n <View\n style={[\n { backgroundColor: colors.background, overflow: 'hidden' },\n screen ? { flex: 1 } : { height: 600, borderRadius: radius.scale(28), borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border },\n style,\n ]}\n {...rest}\n />\n </ScreenMode.Provider>\n );\n}\n\n/** Fake status bar: time, signal and battery, drawn with Views. */\nfunction StatusBar() {\n const { colors, radius } = useKit();\n const ink = colors.foreground;\n return (\n <View style={{ height: STATUS_H, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 15, color: ink, fontVariant: ['tabular-nums'] }}>9:41</Text>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2 }}>\n {[4, 6, 8, 10].map((h) => (\n <View key={h} style={{ width: 3, height: h, borderRadius: radius.capsule(3), backgroundColor: ink }} />\n ))}\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>\n <View style={{ width: 22, height: 11, padding: 1.5, borderRadius: radius.scale(3.5), borderWidth: 1, borderColor: alpha(ink, 0.4) }}>\n <View style={{ width: '72%', flex: 1, borderRadius: radius.scale(2), backgroundColor: ink }} />\n </View>\n <View style={{ width: 1.5, height: 4, borderRadius: 1, backgroundColor: alpha(ink, 0.4) }} />\n </View>\n </View>\n </View>\n );\n}\n\n/** The app screen behind the drawer: status bar plus your content. */\nfunction Phone({ children }: { children: React.ReactNode }) {\n const { colors } = useKit();\n const screen = React.useContext(ScreenMode);\n return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n {screen ? null : <StatusBar />}\n {children}\n </View>\n );\n}\n\nfunction IconButton({ icon, label, onPress, color }: { icon: DrawerIconName; label: string; onPress?: () => void; color?: string }) {\n const { colors, radius } = useKit();\n return (\n <Pressable\n onPress={onPress}\n accessibilityRole=\"button\"\n accessibilityLabel={label}\n style={({ pressed }) => ({\n width: 44,\n height: 44,\n borderRadius: radius.capsule(44),\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: pressed ? colors.muted : 'transparent',\n })}\n >\n <DrawerIcon name={icon} size={22} color={color ?? colors.foreground} />\n </Pressable>\n );\n}\n\n/** The hamburger: toggles the surrounding Drawer through useDrawer(). */\nfunction MenuButton() {\n const { open, setOpen } = useDrawer();\n return <IconButton icon=\"menu\" label=\"Open navigation\" onPress={() => setOpen(!open)} />;\n}\n\n/** A plain inline top bar: hamburger, title (+ subtitle) and trailing actions. */\nfunction TopBar({ title, subtitle, right, menu = true }: { title: string; subtitle?: string; right?: React.ReactNode; menu?: boolean }) {\n const { colors } = useKit();\n return (\n <View style={{ minHeight: 56, flexDirection: 'row', alignItems: 'center', paddingLeft: menu ? 6 : 16, paddingRight: 8, gap: 4 }}>\n {menu ? <MenuButton /> : null}\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: subtitle ? 19 : 22, letterSpacing: -0.4, color: colors.foreground }}>\n {title}\n </Text>\n {subtitle ? (\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: 12.5, color: colors.mutedForeground }}>\n {subtitle}\n </Text>\n ) : null}\n </View>\n {right}\n </View>\n );\n}\n\ntype ArtKind = 'sunset' | 'grid' | 'rings' | 'stripes' | 'split';\n\n/**\n * Album art drawn with plain Views in theme colours. `size` 0 stretches to the parent width\n * (then pass `height`).\n */\nfunction Art({ kind, size, height, radiusSize }: { kind: ArtKind; size: number; height?: number; radiusSize?: number }) {\n const { colors, radius } = useKit();\n const h = height ?? size;\n const w = size || undefined;\n const d = Math.min(size || h, h);\n const box = {\n width: w,\n alignSelf: size ? undefined : ('stretch' as const),\n height: h,\n borderRadius: radiusSize ?? radius.scale(size > 60 ? 14 : 8),\n overflow: 'hidden' as const,\n };\n const on = colors.accentForeground;\n if (kind === 'sunset') {\n return (\n <View style={[box, { backgroundColor: colors.accent, alignItems: 'center' }]}>\n <View style={{ position: 'absolute', top: h * 0.3, width: d * 0.5, height: d * 0.5, borderRadius: radius.capsule(d), backgroundColor: alpha(on, 0.85) }} />\n <View style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: h * 0.34, backgroundColor: alpha(colors.foreground, 0.35) }} />\n <View style={{ position: 'absolute', left: 0, right: 0, bottom: h * 0.22, height: Math.max(1, h * 0.03), backgroundColor: alpha(on, 0.35) }} />\n </View>\n );\n }\n if (kind === 'grid') {\n const cell = d * 0.18;\n return (\n <View style={[box, { backgroundColor: colors.foreground, alignItems: 'center', justifyContent: 'center' }]}>\n <View style={{ width: cell * 3 + cell * 0.5 * 2, flexDirection: 'row', flexWrap: 'wrap', gap: cell * 0.5 }}>\n {[1, 0.25, 0.6, 0.25, 1, 0.25, 0.6, 0.25, 1].map((a, i) => (\n <View key={i} style={{ width: cell, height: cell, borderRadius: radius.scale(cell * 0.3), backgroundColor: alpha(colors.accent, a) }} />\n ))}\n </View>\n </View>\n );\n }\n if (kind === 'rings') {\n return (\n <View style={[box, { backgroundColor: alpha(colors.accent, 0.16), alignItems: 'center', justifyContent: 'center' }]}>\n {[0.95, 0.7, 0.45, 0.2].map((f) => (\n <View\n key={f}\n style={{\n position: 'absolute',\n width: d * f,\n height: d * f,\n borderRadius: radius.capsule(d),\n borderWidth: Math.max(1.5, d * 0.03),\n borderColor: alpha(colors.accent, 1 - f * 0.6),\n }}\n />\n ))}\n </View>\n );\n }\n if (kind === 'stripes') {\n return (\n <View style={[box, { backgroundColor: colors.muted, justifyContent: 'center', alignItems: 'center' }]}>\n <View style={{ width: d * 1.6, gap: d * 0.07, transform: [{ rotate: '-30deg' }] }}>\n {[0.9, 0.55, 0.3, 0.55, 0.9].map((a, i) => (\n <View key={i} style={{ height: d * 0.1, backgroundColor: alpha(colors.accent, a) }} />\n ))}\n </View>\n </View>\n );\n }\n return (\n <View style={[box, { backgroundColor: colors.foreground, flexDirection: 'row' }]}>\n <View style={{ flex: 1, backgroundColor: colors.accent }} />\n <View style={{ flex: 1 }} />\n <View\n style={{\n position: 'absolute',\n left: (w ?? d) / 2 - d * 0.22,\n top: h / 2 - d * 0.22,\n width: d * 0.44,\n height: d * 0.44,\n borderRadius: radius.capsule(d),\n backgroundColor: colors.background,\n }}\n />\n </View>\n );\n}\n\nexport default function RightPanel({ screen, bottomInset }: DemoProps) {\n const { colors, radius } = useKit();\n const [open, setOpen] = useState(false);\n const [price, setPrice] = useState('Any price');\n const [tags, setTags] = useState<string[]>(['Free cancellation']);\n const toggle = (t: string) => setTags((s) => (s.includes(t) ? s.filter((x) => x !== t) : [...s, t]));\n const count = 48 - tags.length * 9 - (price === 'Any price' ? 0 : 11);\n const active = tags.length + (price !== 'Any price' ? 1 : 0);\n return (\n <Frame screen={screen}>\n <Drawer\n open={open}\n onOpenChange={setOpen}\n side=\"right\"\n width=\"86%\"\n topInset={screen ? undefined : STATUS_H}\n bottomInset={bottomInset}\n accessibilityLabel=\"Filters\"\n header={<DrawerHeader title=\"Filters\" subtitle=\"Porto · Jun 3 – 7 · 2 guests\" showClose onClose={() => setOpen(false)} />}\n content={\n <>\n <DrawerSection title=\"Price per night\" divider={false} />\n {['Any price', 'Under ₹5,000', '₹5,000 – ₹10,000', '₹10,000+'].map((p) => (\n <DrawerItem key={p} label={p} icon={price === p ? 'check' : <View />} active={price === p} closeOnPress={false} onPress={() => setPrice(p)} />\n ))}\n <DrawerSection title=\"Amenities\" />\n {['Free cancellation', 'Breakfast included', 'Pet friendly', 'Workspace'].map((t) => (\n <DrawerItem key={t} label={t} icon={tags.includes(t) ? 'check' : <View />} active={tags.includes(t)} closeOnPress={false} onPress={() => toggle(t)} />\n ))}\n </>\n }\n footer={\n <View style={{ flexDirection: 'row', gap: 8, paddingHorizontal: 4 }}>\n <Button\n variant=\"outline\"\n style={{ flex: 1 }}\n onPress={() => {\n setPrice('Any price');\n setTags([]);\n }}\n >\n Reset\n </Button>\n <Button variant=\"accent\" style={{ flex: 1.4 }} onPress={() => setOpen(false)}>\n {`Show ${count} stays`}\n </Button>\n </View>\n }\n >\n <Phone>\n <TopBar\n menu={false}\n title=\"Stays in Porto\"\n subtitle={`${count} places · Jun 3 – 7`}\n right={\n <Button size=\"sm\" variant=\"secondary\" pill left={<DrawerIcon name=\"filter\" size={16} />} onPress={() => setOpen(true)}>\n {active ? `Filters · ${active}` : 'Filters'}\n </Button>\n }\n />\n <View style={{ paddingHorizontal: 16, paddingTop: 4, gap: 16 }}>\n {[\n { name: 'Casa da Ribeira', meta: 'River view · 1 bedroom', price: '₹6,800', rating: '4.92', art: 'sunset' as const },\n { name: 'Azulejo Loft', meta: 'Breakfast included', price: '₹4,800', rating: '4.81', art: 'grid' as const },\n { name: 'Miradouro Suites', meta: 'Free cancellation', price: '₹12,500', rating: '4.97', art: 'rings' as const },\n ].map((s) => (\n <View key={s.name} style={{ gap: 8 }}>\n <Art kind={s.art} size={0} height={112} radiusSize={radius.scale(18)} />\n <View style={{ flexDirection: 'row', alignItems: 'flex-start', gap: 8 }}>\n <View style={{ flex: 1 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 15, color: colors.foreground }}>{s.name}</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 13, color: colors.mutedForeground }}>{s.meta}</Text>\n </View>\n <View style={{ alignItems: 'flex-end' }}>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>\n <DrawerIcon name=\"star\" size={13} color={colors.foreground} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 13, color: colors.foreground }}>{s.rating}</Text>\n </View>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 13, color: colors.mutedForeground }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: colors.foreground }}>{s.price}</Text> night\n </Text>\n </View>\n </View>\n </View>\n ))}\n </View>\n </Phone>\n </Drawer>\n </Frame>\n );\n}\n\n/* Music ----------------------------------------------------------------------------------------- */\n"
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
"name": "PushLayout",
|
|
195
195
|
"title": "Push Layout",
|
|
196
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type ViewProps } from 'react-native';\nimport {\n Drawer,\n DrawerFooter,\n DrawerHeader,\n DrawerIcon,\n DrawerItem,\n DrawerProfile,\n DrawerSection,\n useDrawer,\n type DrawerIconName,\n} from '@/components/ui/drawer';\nimport { alpha, useKit } from '@/lib/kit';\n\nconst STATUS_H = 40;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\n/** True when the demo fills a real phone screen: the frame and the fake status bar step aside. */\nconst ScreenMode = React.createContext(false);\n\n/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, screen = false, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <ScreenMode.Provider value={screen}>\n <View\n style={[\n { backgroundColor: colors.background, overflow: 'hidden' },\n screen ? { flex: 1 } : { height: 600, borderRadius: radius.scale(28), borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border },\n style,\n ]}\n {...rest}\n />\n </ScreenMode.Provider>\n );\n}\n\n/** Fake status bar: time, signal and battery, drawn with Views. */\nfunction StatusBar() {\n const { colors, radius } = useKit();\n const ink = colors.foreground;\n return (\n <View style={{ height: STATUS_H, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24 }}>\n <Text style={{ fontSize: 15, color: ink, fontVariant: ['tabular-nums'] }}>9:41</Text>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2 }}>\n {[4, 6, 8, 10].map((h) => (\n <View key={h} style={{ width: 3, height: h, borderRadius: radius.capsule(3), backgroundColor: ink }} />\n ))}\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>\n <View style={{ width: 22, height: 11, padding: 1.5, borderRadius: radius.scale(3.5), borderWidth: 1, borderColor: alpha(ink, 0.4) }}>\n <View style={{ width: '72%', flex: 1, borderRadius: radius.scale(2), backgroundColor: ink }} />\n </View>\n <View style={{ width: 1.5, height: 4, borderRadius: 1, backgroundColor: alpha(ink, 0.4) }} />\n </View>\n </View>\n </View>\n );\n}\n\n/** The app screen behind the drawer: status bar plus your content. */\nfunction Phone({ children }: { children: React.ReactNode }) {\n const { colors } = useKit();\n const screen = React.useContext(ScreenMode);\n return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n {screen ? null : <StatusBar />}\n {children}\n </View>\n );\n}\n\nfunction IconButton({ icon, label, onPress, color }: { icon: DrawerIconName; label: string; onPress?: () => void; color?: string }) {\n const { colors, radius } = useKit();\n return (\n <Pressable\n onPress={onPress}\n accessibilityRole=\"button\"\n accessibilityLabel={label}\n style={({ pressed }) => ({\n width: 44,\n height: 44,\n borderRadius: radius.capsule(44),\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: pressed ? colors.muted : 'transparent',\n })}\n >\n <DrawerIcon name={icon} size={22} color={color ?? colors.foreground} />\n </Pressable>\n );\n}\n\n/** The hamburger: toggles the surrounding Drawer through useDrawer(). */\nfunction MenuButton() {\n const { open, setOpen } = useDrawer();\n return <IconButton icon=\"menu\" label=\"Open navigation\" onPress={() => setOpen(!open)} />;\n}\n\n/** A plain inline top bar: hamburger, title (+ subtitle) and trailing actions. */\nfunction TopBar({ title, subtitle, right, menu = true }: { title: string; subtitle?: string; right?: React.ReactNode; menu?: boolean }) {\n const { colors } = useKit();\n return (\n <View style={{ height: 56, flexDirection: 'row', alignItems: 'center', paddingLeft: menu ? 6 : 16, paddingRight: 8, gap: 4 }}>\n {menu ? <MenuButton /> : null}\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text numberOfLines={1} style={{ fontSize: subtitle ? 19 : 22, letterSpacing: -0.4, color: colors.foreground }}>\n {title}\n </Text>\n {subtitle ? (\n <Text numberOfLines={1} style={{ fontSize: 12.5, color: colors.mutedForeground }}>\n {subtitle}\n </Text>\n ) : null}\n </View>\n {right}\n </View>\n );\n}\n\ntype ArtKind = 'sunset' | 'grid' | 'rings' | 'stripes' | 'split';\n\n/**\n * Album art drawn with plain Views in theme colours. `size` 0 stretches to the parent width\n * (then pass `height`).\n */\nfunction Art({ kind, size, height, radiusSize }: { kind: ArtKind; size: number; height?: number; radiusSize?: number }) {\n const { colors, radius } = useKit();\n const h = height ?? size;\n const w = size || undefined;\n const d = Math.min(size || h, h);\n const box = {\n width: w,\n alignSelf: size ? undefined : ('stretch' as const),\n height: h,\n borderRadius: radiusSize ?? radius.scale(size > 60 ? 14 : 8),\n overflow: 'hidden' as const,\n };\n const on = colors.accentForeground;\n if (kind === 'sunset') {\n return (\n <View style={[box, { backgroundColor: colors.accent, alignItems: 'center' }]}>\n <View style={{ position: 'absolute', top: h * 0.3, width: d * 0.5, height: d * 0.5, borderRadius: radius.capsule(d), backgroundColor: alpha(on, 0.85) }} />\n <View style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: h * 0.34, backgroundColor: alpha(colors.foreground, 0.35) }} />\n <View style={{ position: 'absolute', left: 0, right: 0, bottom: h * 0.22, height: Math.max(1, h * 0.03), backgroundColor: alpha(on, 0.35) }} />\n </View>\n );\n }\n if (kind === 'grid') {\n const cell = d * 0.18;\n return (\n <View style={[box, { backgroundColor: colors.foreground, alignItems: 'center', justifyContent: 'center' }]}>\n <View style={{ width: cell * 3 + cell * 0.5 * 2, flexDirection: 'row', flexWrap: 'wrap', gap: cell * 0.5 }}>\n {[1, 0.25, 0.6, 0.25, 1, 0.25, 0.6, 0.25, 1].map((a, i) => (\n <View key={i} style={{ width: cell, height: cell, borderRadius: radius.scale(cell * 0.3), backgroundColor: alpha(colors.accent, a) }} />\n ))}\n </View>\n </View>\n );\n }\n if (kind === 'rings') {\n return (\n <View style={[box, { backgroundColor: alpha(colors.accent, 0.16), alignItems: 'center', justifyContent: 'center' }]}>\n {[0.95, 0.7, 0.45, 0.2].map((f) => (\n <View\n key={f}\n style={{\n position: 'absolute',\n width: d * f,\n height: d * f,\n borderRadius: radius.capsule(d),\n borderWidth: Math.max(1.5, d * 0.03),\n borderColor: alpha(colors.accent, 1 - f * 0.6),\n }}\n />\n ))}\n </View>\n );\n }\n if (kind === 'stripes') {\n return (\n <View style={[box, { backgroundColor: colors.muted, justifyContent: 'center', alignItems: 'center' }]}>\n <View style={{ width: d * 1.6, gap: d * 0.07, transform: [{ rotate: '-30deg' }] }}>\n {[0.9, 0.55, 0.3, 0.55, 0.9].map((a, i) => (\n <View key={i} style={{ height: d * 0.1, backgroundColor: alpha(colors.accent, a) }} />\n ))}\n </View>\n </View>\n );\n }\n return (\n <View style={[box, { backgroundColor: colors.foreground, flexDirection: 'row' }]}>\n <View style={{ flex: 1, backgroundColor: colors.accent }} />\n <View style={{ flex: 1 }} />\n <View\n style={{\n position: 'absolute',\n left: (w ?? d) / 2 - d * 0.22,\n top: h / 2 - d * 0.22,\n width: d * 0.44,\n height: d * 0.44,\n borderRadius: radius.capsule(d),\n backgroundColor: colors.background,\n }}\n />\n </View>\n );\n}\n\n/** A right-pointing triangle made from borders (play / skip glyphs). */\nfunction Tri({ size, color, flip }: { size: number; color: string; flip?: boolean }) {\n return (\n <View\n style={{\n width: 0,\n height: 0,\n borderTopWidth: size / 2,\n borderBottomWidth: size / 2,\n borderLeftWidth: size * 0.85,\n borderTopColor: 'transparent',\n borderBottomColor: 'transparent',\n borderLeftColor: color,\n transform: flip ? [{ scaleX: -1 }] : undefined,\n }}\n />\n );\n}\n\nfunction Skip({ color, back }: { color: string; back?: boolean }) {\n const { radius } = useKit();\n return (\n <View style={{ flexDirection: back ? 'row-reverse' : 'row', alignItems: 'center', gap: 1 }}>\n <Tri size={14} color={color} flip={back} />\n <View style={{ width: 2.5, height: 14, borderRadius: radius.capsule(2.5), backgroundColor: color }} />\n </View>\n );\n}\n\nconst TRACKS: { title: string; artist: string; time: string; art: ArtKind; playing?: boolean }[] = [\n { title: 'Midnight City Lights', artist: 'Nova Arcade', time: '4:12', art: 'sunset', playing: true },\n { title: 'Paper Planes', artist: 'Juniper Fields', time: '3:27', art: 'rings' },\n { title: 'Tidal', artist: 'Okra & The Moon', time: '5:03', art: 'stripes' },\n { title: 'Slow Burn', artist: 'Halcyon Days', time: '3:48', art: 'grid' },\n { title: 'Northern Line', artist: 'The Lanterns', time: '4:31', art: 'split' },\n];\n\nexport default function PushLayout({ screen, bottomInset }: DemoProps) {\n const { colors, radius, dark } = useKit();\n const [view, setView] = useState('Listen now');\n const [playing, setPlaying] = useState(true);\n return (\n <Frame screen={screen}>\n <Drawer\n variant=\"push\"\n width={252}\n topInset={screen ? undefined : STATUS_H}\n bottomInset={bottomInset}\n header={<DrawerProfile layout=\"stacked\" name=\"Jonas Weber\" email=\"Premium · 2 devices\" />}\n content={\n <>\n {(\n [\n ['home', 'Listen now'],\n ['search', 'Search'],\n ['music', 'Library', 3],\n ['bell', 'New releases', 8, 'accent'],\n ] as [DrawerIconName, string, number?, 'accent'?][]\n ).map(([icon, label, badge, badgeTone]) => (\n <DrawerItem key={label} icon={icon} label={label} badge={badge} badgeTone={badgeTone} active={view === label} onPress={() => setView(label)} />\n ))}\n <DrawerSection title=\"Playlists\" />\n {['Late Night Drive', 'Focus Flow', 'Sunday Coffee'].map((p, i) => (\n <DrawerItem\n key={p}\n icon={<Art kind={(['sunset', 'rings', 'stripes'] as const)[i]} size={22} radiusSize={radius.scale(5)} />}\n label={p}\n active={view === p}\n onPress={() => setView(p)}\n />\n ))}\n </>\n }\n footer={<DrawerItem icon=\"help\" label=\"Help & feedback\" closeOnPress={false} />}\n >\n <Phone>\n <TopBar title={view} right={<IconButton icon=\"search\" label=\"Search music\" />} />\n <View style={{ paddingHorizontal: 16, gap: 14 }}>\n <View\n style={{\n padding: 14,\n gap: 12,\n borderRadius: radius.scale(20),\n backgroundColor: colors.card,\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: colors.border,\n shadowColor: dark ? colors.background : colors.foreground,\n shadowOpacity: dark ? 0.4 : 0.06,\n shadowRadius: 14,\n shadowOffset: { width: 0, height: 4 },\n elevation: 2,\n }}\n >\n <View style={{ flexDirection: 'row', gap: 14 }}>\n <Art kind=\"sunset\" size={92} />\n <View style={{ flex: 1, minWidth: 0, justifyContent: 'center', gap: 2 }}>\n <Text style={{ fontSize: 12, color: colors.accent }}>Now playing</Text>\n <Text numberOfLines={1} style={{ fontSize: 17, letterSpacing: -0.3, color: colors.foreground }}>\n Midnight City Lights\n </Text>\n <Text numberOfLines={1} style={{ fontSize: 13.5, color: colors.mutedForeground }}>\n Nova Arcade · Afterglow\n </Text>\n </View>\n </View>\n <View style={{ gap: 6 }}>\n <View style={{ height: 4, borderRadius: radius.capsule(4), backgroundColor: colors.muted, overflow: 'hidden' }}>\n <View style={{ width: '43%', height: '100%', borderRadius: radius.capsule(4), backgroundColor: colors.accent }} />\n </View>\n <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n <Text style={{ fontSize: 11.5, color: colors.mutedForeground, fontVariant: ['tabular-nums'] }}>1:48</Text>\n <Text style={{ fontSize: 11.5, color: colors.mutedForeground, fontVariant: ['tabular-nums'] }}>-2:24</Text>\n </View>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 36 }}>\n <Skip color={colors.foreground} back />\n <Pressable\n onPress={() => setPlaying((v) => !v)}\n accessibilityRole=\"button\"\n accessibilityLabel={playing ? 'Pause' : 'Play'}\n style={{ width: 48, height: 48, borderRadius: radius.capsule(48), backgroundColor: colors.accent, alignItems: 'center', justifyContent: 'center' }}\n >\n {playing ? (\n <View style={{ flexDirection: 'row', gap: 5 }}>\n <View style={{ width: 4.5, height: 16, borderRadius: radius.capsule(4.5), backgroundColor: colors.accentForeground }} />\n <View style={{ width: 4.5, height: 16, borderRadius: radius.capsule(4.5), backgroundColor: colors.accentForeground }} />\n </View>\n ) : (\n <View style={{ marginLeft: 3 }}>\n <Tri size={17} color={colors.accentForeground} />\n </View>\n )}\n </Pressable>\n <Skip color={colors.foreground} />\n </View>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'baseline', justifyContent: 'space-between' }}>\n <Text style={{ fontSize: 17, letterSpacing: -0.3, color: colors.foreground }}>Up next</Text>\n <Text style={{ fontSize: 13.5, color: colors.accent }}>See all</Text>\n </View>\n <View style={{ gap: 12 }}>\n {TRACKS.map((t) => (\n <View key={t.title} style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>\n <Art kind={t.art} size={44} />\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text numberOfLines={1} style={{ fontSize: 15, color: t.playing ? colors.accent : colors.foreground }}>\n {t.title}\n </Text>\n <Text numberOfLines={1} style={{ fontSize: 13, color: colors.mutedForeground }}>\n {t.artist}\n </Text>\n </View>\n {t.playing ? (\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2, height: 14 }}>\n {[8, 14, 10].map((hh, i) => (\n <View key={i} style={{ width: 3, height: hh, borderRadius: radius.capsule(3), backgroundColor: colors.accent }} />\n ))}\n </View>\n ) : (\n <Text style={{ fontSize: 13, color: colors.mutedForeground, fontVariant: ['tabular-nums'] }}>{t.time}</Text>\n )}\n </View>\n ))}\n </View>\n </View>\n </Phone>\n </Drawer>\n </Frame>\n );\n}\n"
|
|
196
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type ViewProps } from 'react-native';\nimport {\n Drawer,\n DrawerFooter,\n DrawerHeader,\n DrawerIcon,\n DrawerItem,\n DrawerProfile,\n DrawerSection,\n useDrawer,\n type DrawerIconName,\n} from '@/components/ui/drawer';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\n/* Shared phone chrome --------------------------------------------------------------------------- */\n\nconst STATUS_H = 40;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\n/** True when the demo fills a real phone screen: the frame and the fake status bar step aside. */\nconst ScreenMode = React.createContext(false);\n\n/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, screen = false, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <ScreenMode.Provider value={screen}>\n <View\n style={[\n { backgroundColor: colors.background, overflow: 'hidden' },\n screen ? { flex: 1 } : { height: 600, borderRadius: radius.scale(28), borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border },\n style,\n ]}\n {...rest}\n />\n </ScreenMode.Provider>\n );\n}\n\n/** Fake status bar: time, signal and battery, drawn with Views. */\nfunction StatusBar() {\n const { colors, radius } = useKit();\n const ink = colors.foreground;\n return (\n <View style={{ height: STATUS_H, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 15, color: ink, fontVariant: ['tabular-nums'] }}>9:41</Text>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2 }}>\n {[4, 6, 8, 10].map((h) => (\n <View key={h} style={{ width: 3, height: h, borderRadius: radius.capsule(3), backgroundColor: ink }} />\n ))}\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>\n <View style={{ width: 22, height: 11, padding: 1.5, borderRadius: radius.scale(3.5), borderWidth: 1, borderColor: alpha(ink, 0.4) }}>\n <View style={{ width: '72%', flex: 1, borderRadius: radius.scale(2), backgroundColor: ink }} />\n </View>\n <View style={{ width: 1.5, height: 4, borderRadius: 1, backgroundColor: alpha(ink, 0.4) }} />\n </View>\n </View>\n </View>\n );\n}\n\n/** The app screen behind the drawer: status bar plus your content. */\nfunction Phone({ children }: { children: React.ReactNode }) {\n const { colors } = useKit();\n const screen = React.useContext(ScreenMode);\n return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n {screen ? null : <StatusBar />}\n {children}\n </View>\n );\n}\n\nfunction IconButton({ icon, label, onPress, color }: { icon: DrawerIconName; label: string; onPress?: () => void; color?: string }) {\n const { colors, radius } = useKit();\n return (\n <Pressable\n onPress={onPress}\n accessibilityRole=\"button\"\n accessibilityLabel={label}\n style={({ pressed }) => ({\n width: 44,\n height: 44,\n borderRadius: radius.capsule(44),\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: pressed ? colors.muted : 'transparent',\n })}\n >\n <DrawerIcon name={icon} size={22} color={color ?? colors.foreground} />\n </Pressable>\n );\n}\n\n/** The hamburger: toggles the surrounding Drawer through useDrawer(). */\nfunction MenuButton() {\n const { open, setOpen } = useDrawer();\n return <IconButton icon=\"menu\" label=\"Open navigation\" onPress={() => setOpen(!open)} />;\n}\n\n/** A plain inline top bar: hamburger, title (+ subtitle) and trailing actions. */\nfunction TopBar({ title, subtitle, right, menu = true }: { title: string; subtitle?: string; right?: React.ReactNode; menu?: boolean }) {\n const { colors } = useKit();\n return (\n <View style={{ minHeight: 56, flexDirection: 'row', alignItems: 'center', paddingLeft: menu ? 6 : 16, paddingRight: 8, gap: 4 }}>\n {menu ? <MenuButton /> : null}\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: subtitle ? 19 : 22, letterSpacing: -0.4, color: colors.foreground }}>\n {title}\n </Text>\n {subtitle ? (\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: 12.5, color: colors.mutedForeground }}>\n {subtitle}\n </Text>\n ) : null}\n </View>\n {right}\n </View>\n );\n}\n\ntype ArtKind = 'sunset' | 'grid' | 'rings' | 'stripes' | 'split';\n\n/**\n * Album art drawn with plain Views in theme colours. `size` 0 stretches to the parent width\n * (then pass `height`).\n */\nfunction Art({ kind, size, height, radiusSize }: { kind: ArtKind; size: number; height?: number; radiusSize?: number }) {\n const { colors, radius } = useKit();\n const h = height ?? size;\n const w = size || undefined;\n const d = Math.min(size || h, h);\n const box = {\n width: w,\n alignSelf: size ? undefined : ('stretch' as const),\n height: h,\n borderRadius: radiusSize ?? radius.scale(size > 60 ? 14 : 8),\n overflow: 'hidden' as const,\n };\n const on = colors.accentForeground;\n if (kind === 'sunset') {\n return (\n <View style={[box, { backgroundColor: colors.accent, alignItems: 'center' }]}>\n <View style={{ position: 'absolute', top: h * 0.3, width: d * 0.5, height: d * 0.5, borderRadius: radius.capsule(d), backgroundColor: alpha(on, 0.85) }} />\n <View style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: h * 0.34, backgroundColor: alpha(colors.foreground, 0.35) }} />\n <View style={{ position: 'absolute', left: 0, right: 0, bottom: h * 0.22, height: Math.max(1, h * 0.03), backgroundColor: alpha(on, 0.35) }} />\n </View>\n );\n }\n if (kind === 'grid') {\n const cell = d * 0.18;\n return (\n <View style={[box, { backgroundColor: colors.foreground, alignItems: 'center', justifyContent: 'center' }]}>\n <View style={{ width: cell * 3 + cell * 0.5 * 2, flexDirection: 'row', flexWrap: 'wrap', gap: cell * 0.5 }}>\n {[1, 0.25, 0.6, 0.25, 1, 0.25, 0.6, 0.25, 1].map((a, i) => (\n <View key={i} style={{ width: cell, height: cell, borderRadius: radius.scale(cell * 0.3), backgroundColor: alpha(colors.accent, a) }} />\n ))}\n </View>\n </View>\n );\n }\n if (kind === 'rings') {\n return (\n <View style={[box, { backgroundColor: alpha(colors.accent, 0.16), alignItems: 'center', justifyContent: 'center' }]}>\n {[0.95, 0.7, 0.45, 0.2].map((f) => (\n <View\n key={f}\n style={{\n position: 'absolute',\n width: d * f,\n height: d * f,\n borderRadius: radius.capsule(d),\n borderWidth: Math.max(1.5, d * 0.03),\n borderColor: alpha(colors.accent, 1 - f * 0.6),\n }}\n />\n ))}\n </View>\n );\n }\n if (kind === 'stripes') {\n return (\n <View style={[box, { backgroundColor: colors.muted, justifyContent: 'center', alignItems: 'center' }]}>\n <View style={{ width: d * 1.6, gap: d * 0.07, transform: [{ rotate: '-30deg' }] }}>\n {[0.9, 0.55, 0.3, 0.55, 0.9].map((a, i) => (\n <View key={i} style={{ height: d * 0.1, backgroundColor: alpha(colors.accent, a) }} />\n ))}\n </View>\n </View>\n );\n }\n return (\n <View style={[box, { backgroundColor: colors.foreground, flexDirection: 'row' }]}>\n <View style={{ flex: 1, backgroundColor: colors.accent }} />\n <View style={{ flex: 1 }} />\n <View\n style={{\n position: 'absolute',\n left: (w ?? d) / 2 - d * 0.22,\n top: h / 2 - d * 0.22,\n width: d * 0.44,\n height: d * 0.44,\n borderRadius: radius.capsule(d),\n backgroundColor: colors.background,\n }}\n />\n </View>\n );\n}\n\n/** A right-pointing triangle made from borders (play / skip glyphs). */\nfunction Tri({ size, color, flip }: { size: number; color: string; flip?: boolean }) {\n return (\n <View\n style={{\n width: 0,\n height: 0,\n borderTopWidth: size / 2,\n borderBottomWidth: size / 2,\n borderLeftWidth: size * 0.85,\n borderTopColor: 'transparent',\n borderBottomColor: 'transparent',\n borderLeftColor: color,\n transform: flip ? [{ scaleX: -1 }] : undefined,\n }}\n />\n );\n}\n\nfunction Skip({ color, back }: { color: string; back?: boolean }) {\n const { radius } = useKit();\n return (\n <View style={{ flexDirection: back ? 'row-reverse' : 'row', alignItems: 'center', gap: 1 }}>\n <Tri size={14} color={color} flip={back} />\n <View style={{ width: 2.5, height: 14, borderRadius: radius.capsule(2.5), backgroundColor: color }} />\n </View>\n );\n}\n\nconst TRACKS: { title: string; artist: string; time: string; art: ArtKind; playing?: boolean }[] = [\n { title: 'Midnight City Lights', artist: 'Nova Arcade', time: '4:12', art: 'sunset', playing: true },\n { title: 'Paper Planes', artist: 'Juniper Fields', time: '3:27', art: 'rings' },\n { title: 'Tidal', artist: 'Okra & The Moon', time: '5:03', art: 'stripes' },\n { title: 'Slow Burn', artist: 'Halcyon Days', time: '3:48', art: 'grid' },\n { title: 'Northern Line', artist: 'The Lanterns', time: '4:31', art: 'split' },\n];\n\nexport default function PushLayout({ screen, bottomInset }: DemoProps) {\n const { colors, radius, dark } = useKit();\n const [view, setView] = useState('Listen now');\n const [playing, setPlaying] = useState(true);\n return (\n <Frame screen={screen}>\n <Drawer\n variant=\"push\"\n width={252}\n topInset={screen ? undefined : STATUS_H}\n bottomInset={bottomInset}\n header={<DrawerProfile layout=\"stacked\" name=\"Jonas Weber\" email=\"Premium · 2 devices\" />}\n content={\n <>\n {(\n [\n ['home', 'Listen now'],\n ['search', 'Search'],\n ['music', 'Library', 3],\n ['bell', 'New releases', 8, 'accent'],\n ] as [DrawerIconName, string, number?, 'accent'?][]\n ).map(([icon, label, badge, badgeTone]) => (\n <DrawerItem key={label} icon={icon} label={label} badge={badge} badgeTone={badgeTone} active={view === label} onPress={() => setView(label)} />\n ))}\n <DrawerSection title=\"Playlists\" />\n {['Late Night Drive', 'Focus Flow', 'Sunday Coffee'].map((p, i) => (\n <DrawerItem\n key={p}\n icon={<Art kind={(['sunset', 'rings', 'stripes'] as const)[i]} size={22} radiusSize={radius.scale(5)} />}\n label={p}\n active={view === p}\n onPress={() => setView(p)}\n />\n ))}\n </>\n }\n footer={<DrawerItem icon=\"help\" label=\"Help & feedback\" closeOnPress={false} />}\n >\n <Phone>\n <TopBar title={view} right={<IconButton icon=\"search\" label=\"Search music\" />} />\n <View style={{ paddingHorizontal: 16, gap: 14 }}>\n <View\n style={{\n padding: 14,\n gap: 12,\n borderRadius: radius.scale(20),\n backgroundColor: colors.card,\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: colors.border,\n shadowColor: dark ? colors.background : colors.foreground,\n shadowOpacity: dark ? 0.4 : 0.06,\n shadowRadius: 14,\n shadowOffset: { width: 0, height: 4 },\n elevation: 2,\n }}\n >\n <View style={{ flexDirection: 'row', gap: 14 }}>\n <Art kind=\"sunset\" size={92} />\n <View style={{ flex: 1, minWidth: 0, justifyContent: 'center', gap: 2 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12, color: colors.accent }}>Now playing</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: 17, letterSpacing: -0.3, color: colors.foreground }}>\n Midnight City Lights\n </Text>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: 13.5, color: colors.mutedForeground }}>\n Nova Arcade · Afterglow\n </Text>\n </View>\n </View>\n <View style={{ gap: 6 }}>\n <View style={{ height: 4, borderRadius: radius.capsule(4), backgroundColor: colors.muted, overflow: 'hidden' }}>\n <View style={{ width: '43%', height: '100%', borderRadius: radius.capsule(4), backgroundColor: colors.accent }} />\n </View>\n <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 11.5, color: colors.mutedForeground, fontVariant: ['tabular-nums'] }}>1:48</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 11.5, color: colors.mutedForeground, fontVariant: ['tabular-nums'] }}>-2:24</Text>\n </View>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 36 }}>\n <Skip color={colors.foreground} back />\n <Pressable\n onPress={() => setPlaying((v) => !v)}\n accessibilityRole=\"button\"\n accessibilityLabel={playing ? 'Pause' : 'Play'}\n style={{ width: 48, height: 48, borderRadius: radius.capsule(48), backgroundColor: colors.accent, alignItems: 'center', justifyContent: 'center' }}\n >\n {playing ? (\n <View style={{ flexDirection: 'row', gap: 5 }}>\n <View style={{ width: 4.5, height: 16, borderRadius: radius.capsule(4.5), backgroundColor: colors.accentForeground }} />\n <View style={{ width: 4.5, height: 16, borderRadius: radius.capsule(4.5), backgroundColor: colors.accentForeground }} />\n </View>\n ) : (\n <View style={{ marginLeft: 3 }}>\n <Tri size={17} color={colors.accentForeground} />\n </View>\n )}\n </Pressable>\n <Skip color={colors.foreground} />\n </View>\n </View>\n <View style={{ flexDirection: 'row', alignItems: 'baseline', justifyContent: 'space-between' }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 17, letterSpacing: -0.3, color: colors.foreground }}>Up next</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 13.5, color: colors.accent }}>See all</Text>\n </View>\n <View style={{ gap: 12 }}>\n {TRACKS.map((t) => (\n <View key={t.title} style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>\n <Art kind={t.art} size={44} />\n <View style={{ flex: 1, minWidth: 0 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: 15, color: t.playing ? colors.accent : colors.foreground }}>\n {t.title}\n </Text>\n <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={{ fontSize: 13, color: colors.mutedForeground }}>\n {t.artist}\n </Text>\n </View>\n {t.playing ? (\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2, height: 14 }}>\n {[8, 14, 10].map((hh, i) => (\n <View key={i} style={{ width: 3, height: hh, borderRadius: radius.capsule(3), backgroundColor: colors.accent }} />\n ))}\n </View>\n ) : (\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 13, color: colors.mutedForeground, fontVariant: ['tabular-nums'] }}>{t.time}</Text>\n )}\n </View>\n ))}\n </View>\n </View>\n </Phone>\n </Drawer>\n </Frame>\n );\n}\n"
|
|
197
197
|
}
|
|
198
198
|
]
|
|
199
199
|
}
|
|
@@ -106,22 +106,22 @@
|
|
|
106
106
|
{
|
|
107
107
|
"name": "Polaroid",
|
|
108
108
|
"title": "Polaroid",
|
|
109
|
-
"code": "import React, { useState } from 'react';\nimport { Image, Platform, Pressable, Text, View } from 'react-native';\nimport { FlipCard } from '@/components/ui/flip-card';\nimport { useKit } from '@/lib/kit';\n\n/** Any photo works (`require()` or a URL). */\nconst photo = (id: number, w: number, h: number) => ({ uri: `https://picsum.photos/id/${id}/${w}/${h}` });\n\nconst hand = Platform.select({ ios: 'Noteworthy', android: 'casual', default: '\"Bradley Hand\", \"Segoe Print\", cursive' });\n\n/** A screen title, as it sits above the card in your app. */\nfunction Header({ title, sub, right }: { title: string; sub?: string; right?: React.ReactNode }) {\n const { colors } = useKit();\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>\n <View style={{ flex: 1 }}>\n <Text style={{ color: colors.foreground, fontSize: 22, fontWeight: '700', letterSpacing: -0.4 }}>{title}</Text>\n {sub ? <Text style={{ color: colors.mutedForeground, fontSize: 13, marginTop: 2 }}>{sub}</Text> : null}\n </View>\n {right}\n </View>\n );\n}\n\nexport default function Polaroid() {\n const { colors } = useKit();\n const paper = { width: 240, padding: 12, paddingBottom: 0, backgroundColor: '#f7f5f0' } as const;\n return (\n <View style={{ gap: 26 }}>\n <Header title=\"Memories\" sub=\"Lisbon · June 2025 · 24 photos\" />\n <FlipCard\n radius={6}\n accessibilityLabel=\"Polaroid of a Lisbon tram\"\n style={{ alignSelf: 'center', transform: [{ rotate: '-2deg' }] }}\n front={\n <View style={paper}>\n <Image alt=\"\" source={photo(419, 480, 480)} style={{ width: 216, height: 216, backgroundColor: '#ddd' }} />\n <Text style={{ fontFamily: hand, fontSize: 19, color: '#2b2b2b', textAlign: 'center', paddingVertical: 14 }}>Lisbon, June ’25</Text>\n </View>\n }\n back={\n <View style={[paper, { flex: 1, padding: 22, backgroundColor: '#efe9dc', justifyContent: 'space-between' }]}>\n <Text style={{ fontFamily: hand, fontSize: 18, lineHeight: 27, color: '#2f2a22' }}>\n Tram 28 all the way up to Graça. Pastéis de nata still warm. Best trip yet — come back next year?\n </Text>\n <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end' }}>\n <Text style={{ fontFamily: hand, fontSize: 16, color: '#5b5245' }}>— M.</Text>\n <View style={{ paddingHorizontal: 8, paddingVertical: 4, borderWidth: 1.5, borderColor: '#b04a3a', borderRadius: 4, transform: [{ rotate: '6deg' }] }}>\n <Text style={{ color: '#b04a3a', fontSize: 10, fontWeight: '800', letterSpacing: 1 }}>LISBOA · 14 JUN</Text>\n </View>\n </View>\n </View>\n }\n />\n <Text style={{ color: colors.mutedForeground, fontSize: 13, textAlign: 'center' }}>Tap the photo to read the note on the back</Text>\n </View>\n );\n}\n"
|
|
109
|
+
"code": "import React, { useState } from 'react';\nimport { Image, Platform, Pressable, Text, View } from 'react-native';\nimport { FlipCard } from '@/components/ui/flip-card';\nimport { useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\n/** Any photo works (`require()` or a URL). */\nconst photo = (id: number, w: number, h: number) => ({ uri: `https://picsum.photos/id/${id}/${w}/${h}` });\n\nconst hand = Platform.select({ ios: 'Noteworthy', android: 'casual', default: '\"Bradley Hand\", \"Segoe Print\", cursive' });\n\n/** A screen title, as it sits above the card in your app. */\nfunction Header({ title, sub, right }: { title: string; sub?: string; right?: React.ReactNode }) {\n const { colors } = useKit();\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>\n <View style={{ flex: 1 }}>\n <Text style={{ color: colors.foreground, fontSize: 22, fontWeight: '700', letterSpacing: -0.4 }}>{title}</Text>\n {sub ? <Text style={{ color: colors.mutedForeground, fontSize: 13, marginTop: 2 }}>{sub}</Text> : null}\n </View>\n {right}\n </View>\n );\n}\n\nexport default function Polaroid() {\n const { colors } = useKit();\n const paper = { width: 240, padding: 12, paddingBottom: 0, backgroundColor: '#f7f5f0' } as const;\n return (\n <View style={{ gap: 26 }}>\n <Header title=\"Memories\" sub=\"Lisbon · June 2025 · 24 photos\" />\n <FlipCard\n radius={6}\n accessibilityLabel=\"Polaroid of a Lisbon tram\"\n style={{ alignSelf: 'center', transform: [{ rotate: '-2deg' }] }}\n front={\n <View style={paper}>\n <Image alt=\"\" source={photo(419, 480, 480)} style={{ width: 216, height: 216, backgroundColor: '#ddd' }} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontFamily: hand, fontSize: 19, color: '#2b2b2b', textAlign: 'center', paddingVertical: 14 }}>Lisbon, June ’25</Text>\n </View>\n }\n back={\n <View style={[paper, { flex: 1, padding: 22, backgroundColor: '#efe9dc', justifyContent: 'space-between' }]}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontFamily: hand, fontSize: 18, lineHeight: 27, color: '#2f2a22' }}>\n Tram 28 all the way up to Graça. Pastéis de nata still warm. Best trip yet — come back next year?\n </Text>\n <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end' }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontFamily: hand, fontSize: 16, color: '#5b5245' }}>— M.</Text>\n <View style={{ paddingHorizontal: 8, paddingVertical: 4, borderWidth: 1.5, borderColor: '#b04a3a', borderRadius: 4, transform: [{ rotate: '6deg' }] }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: '#b04a3a', fontSize: 10, fontWeight: '800', letterSpacing: 1 }}>LISBOA · 14 JUN</Text>\n </View>\n </View>\n </View>\n }\n />\n <Text style={{ color: colors.mutedForeground, fontSize: 13, textAlign: 'center' }}>Tap the photo to read the note on the back</Text>\n </View>\n );\n}\n"
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
"name": "Recipe",
|
|
113
113
|
"title": "Recipe",
|
|
114
|
-
"code": "import React, { useState } from 'react';\nimport { Image, Platform, Pressable, Text, View } from 'react-native';\nimport { FlipCard } from '@/components/ui/flip-card';\nimport { useKit } from '@/lib/kit';\n\n/** Any photo works (`require()` or a URL). */\nconst photo = (id: number, w: number, h: number) => ({ uri: `https://picsum.photos/id/${id}/${w}/${h}` });\n\n/** Smooth darkening toward the bottom of a photo, made from thin bands. */\nfunction Scrim({ height = '55%', max = 0.75 }: { height?: `${number}%` | number; max?: number }) {\n const steps = 40; // fine steps: coarse ones read as faint lines on iOS\n return (\n <View pointerEvents=\"none\" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height }}>\n {Array.from({ length: steps }, (_, i) => (\n <View key={i} style={{ flex: 1, backgroundColor: `rgba(0,0,0,${(max * ((i + 0.5) / steps) ** 1.6).toFixed(3)})` }} />\n ))}\n </View>\n );\n}\n\n/** A screen title, as it sits above the card in your app. */\nfunction Header({ title, sub, right }: { title: string; sub?: string; right?: React.ReactNode }) {\n const { colors } = useKit();\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>\n <View style={{ flex: 1 }}>\n <Text style={{ color: colors.foreground, fontSize: 22, fontWeight: '700', letterSpacing: -0.4 }}>{title}</Text>\n {sub ? <Text style={{ color: colors.mutedForeground, fontSize: 13, marginTop: 2 }}>{sub}</Text> : null}\n </View>\n {right}\n </View>\n );\n}\n\nexport default function Recipe() {\n const { colors } = useKit();\n const items = ['3 parsnips, halved', '2 red onions, quartered', '4 carrots', 'Parsley & dill', 'Olive oil, salt, cracked pepper'];\n return (\n <View style={{ gap: 16 }}>\n <Header title=\"Tonight’s dinner\" sub=\"Picked for you · vegetarian\" />\n <FlipCard\n radius={22}\n accessibilityLabel=\"Roasted root vegetables recipe\"\n front={\n <View style={{ height: 260, backgroundColor: '#222' }}>\n <Image alt=\"\" source={photo(292, 720, 520)} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} />\n <Scrim />\n <View style={{ flex: 1, justifyContent: 'flex-end', padding: 18, gap: 2 }}>\n <Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: 11, fontWeight: '700', letterSpacing: 1 }}>45 MIN · SERVES 4</Text>\n <Text style={{ color: '#fff', fontSize: 26, fontWeight: '800', letterSpacing: -0.5 }}>Roasted roots</Text>\n <Text style={{ color: 'rgba(255,255,255,0.75)', fontSize: 12 }}>Tap for ingredients</Text>\n </View>\n </View>\n }\n back={\n <View style={{ flex: 1, padding: 20, backgroundColor: '#f6f1e7', gap: 10 }}>\n <Text style={{ color: '#3b3226', fontSize: 12, fontWeight: '800', letterSpacing: 1.2 }}>INGREDIENTS</Text>\n {items.map((it) => (\n <View key={it} style={{ flexDirection: 'row', alignItems: 'center', gap: 10, paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: 'rgba(59,50,38,0.12)' }}>\n <View style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: '#c9a227' }} />\n <Text style={{ color: '#3b3226', fontSize: 15 }}>{it}</Text>\n </View>\n ))}\n </View>\n }\n />\n <View style={{ flexDirection: 'row', gap: 8 }}>\n {['45 min', 'Serves 4', '520 kcal'].map((t) => (\n <View key={t} style={{ flex: 1, alignItems: 'center', paddingVertical: 9, borderRadius: 12, backgroundColor: colors.muted }}>\n <Text style={{ color: colors.foreground, fontSize: 13, fontWeight: '600' }}>{t}</Text>\n </View>\n ))}\n </View>\n <Pressable accessibilityRole=\"button\" style={{ alignItems: 'center', paddingVertical: 14, borderRadius: 14, backgroundColor: colors.primary }}>\n <Text style={{ color: colors.primaryForeground, fontSize: 15, fontWeight: '600' }}>Start cooking</Text>\n </Pressable>\n </View>\n );\n}\n"
|
|
114
|
+
"code": "import React, { useState } from 'react';\nimport { Image, Platform, Pressable, Text, View } from 'react-native';\nimport { FlipCard } from '@/components/ui/flip-card';\nimport { useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\n/** Any photo works (`require()` or a URL). */\nconst photo = (id: number, w: number, h: number) => ({ uri: `https://picsum.photos/id/${id}/${w}/${h}` });\n\n/** Smooth darkening toward the bottom of a photo, made from thin bands. */\nfunction Scrim({ height = '55%', max = 0.75 }: { height?: `${number}%` | number; max?: number }) {\n const steps = 40; // fine steps: coarse ones read as faint lines on iOS\n return (\n <View pointerEvents=\"none\" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height }}>\n {Array.from({ length: steps }, (_, i) => (\n <View key={i} style={{ flex: 1, backgroundColor: `rgba(0,0,0,${(max * ((i + 0.5) / steps) ** 1.6).toFixed(3)})` }} />\n ))}\n </View>\n );\n}\n\n/** A screen title, as it sits above the card in your app. */\nfunction Header({ title, sub, right }: { title: string; sub?: string; right?: React.ReactNode }) {\n const { colors } = useKit();\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>\n <View style={{ flex: 1 }}>\n <Text style={{ color: colors.foreground, fontSize: 22, fontWeight: '700', letterSpacing: -0.4 }}>{title}</Text>\n {sub ? <Text style={{ color: colors.mutedForeground, fontSize: 13, marginTop: 2 }}>{sub}</Text> : null}\n </View>\n {right}\n </View>\n );\n}\n\nexport default function Recipe() {\n const { colors } = useKit();\n const items = ['3 parsnips, halved', '2 red onions, quartered', '4 carrots', 'Parsley & dill', 'Olive oil, salt, cracked pepper'];\n return (\n <View style={{ gap: 16 }}>\n <Header title=\"Tonight’s dinner\" sub=\"Picked for you · vegetarian\" />\n <FlipCard\n radius={22}\n accessibilityLabel=\"Roasted root vegetables recipe\"\n front={\n <View style={{ height: 260, backgroundColor: '#222' }}>\n <Image alt=\"\" source={photo(292, 720, 520)} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} />\n <Scrim />\n <View style={{ flex: 1, justifyContent: 'flex-end', padding: 18, gap: 2 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: 'rgba(255,255,255,0.8)', fontSize: 11, fontWeight: '700', letterSpacing: 1 }}>45 MIN · SERVES 4</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: '#fff', fontSize: 26, fontWeight: '800', letterSpacing: -0.5 }}>Roasted roots</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: 'rgba(255,255,255,0.75)', fontSize: 12 }}>Tap for ingredients</Text>\n </View>\n </View>\n }\n back={\n <View style={{ flex: 1, padding: 20, backgroundColor: '#f6f1e7', gap: 10 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: '#3b3226', fontSize: 12, fontWeight: '800', letterSpacing: 1.2 }}>INGREDIENTS</Text>\n {items.map((it) => (\n <View key={it} style={{ flexDirection: 'row', alignItems: 'center', gap: 10, paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: 'rgba(59,50,38,0.12)' }}>\n <View style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: '#c9a227' }} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: '#3b3226', fontSize: 15 }}>{it}</Text>\n </View>\n ))}\n </View>\n }\n />\n <View style={{ flexDirection: 'row', gap: 8 }}>\n {['45 min', 'Serves 4', '520 kcal'].map((t) => (\n <View key={t} style={{ flex: 1, alignItems: 'center', paddingVertical: 9, borderRadius: 12, backgroundColor: colors.muted }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: colors.foreground, fontSize: 13, fontWeight: '600' }}>{t}</Text>\n </View>\n ))}\n </View>\n <Pressable accessibilityRole=\"button\" style={{ alignItems: 'center', paddingVertical: 14, borderRadius: 14, backgroundColor: colors.primary }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: colors.primaryForeground, fontSize: 15, fontWeight: '600' }}>Start cooking</Text>\n </Pressable>\n </View>\n );\n}\n"
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
"name": "Profile",
|
|
118
118
|
"title": "Profile",
|
|
119
|
-
"code": "import React, { useState } from 'react';\nimport { Image, Platform, Pressable, Text, View } from 'react-native';\nimport { FlipCard } from '@/components/ui/flip-card';\nimport { useKit } from '@/lib/kit';\n\n/** Any photo works (`require()` or a URL). */\nconst photo = (id: number, w: number, h: number) => ({ uri: `https://picsum.photos/id/${id}/${w}/${h}` });\n\n/** Smooth darkening toward the bottom of a photo, made from thin bands. */\nfunction Scrim({ height = '55%', max = 0.75 }: { height?: `${number}%` | number; max?: number }) {\n const steps = 40; // fine steps: coarse ones read as faint lines on iOS\n return (\n <View pointerEvents=\"none\" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height }}>\n {Array.from({ length: steps }, (_, i) => (\n <View key={i} style={{ flex: 1, backgroundColor: `rgba(0,0,0,${(max * ((i + 0.5) / steps) ** 1.6).toFixed(3)})` }} />\n ))}\n </View>\n );\n}\n\n/** A screen title, as it sits above the card in your app. */\nfunction Header({ title, sub, right }: { title: string; sub?: string; right?: React.ReactNode }) {\n const { colors } = useKit();\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>\n <View style={{ flex: 1 }}>\n <Text style={{ color: colors.foreground, fontSize: 22, fontWeight: '700', letterSpacing: -0.4 }}>{title}</Text>\n {sub ? <Text style={{ color: colors.mutedForeground, fontSize: 13, marginTop: 2 }}>{sub}</Text> : null}\n </View>\n {right}\n </View>\n );\n}\n\nexport default function Profile() {\n const { colors } = useKit();\n return (\n <View style={{ gap: 20 }}>\n <Header title=\"Design team\" sub=\"Tap a card to see their year\" />\n <FlipCard\n radius={24}\n accessibilityLabel=\"Ana Lima, lead designer\"\n style={{ alignSelf: 'center', width: 250 }}\n front={\n <View style={{ height: 320, backgroundColor: '#222' }}>\n <Image alt=\"\" source={photo(1027, 500, 640)} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} />\n <Scrim />\n <View style={{ flex: 1, justifyContent: 'flex-end', padding: 18 }}>\n <Text style={{ color: '#fff', fontSize: 24, fontWeight: '800', letterSpacing: -0.4 }}>Ana Lima</Text>\n <Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: 13 }}>Lead designer · Lisbon</Text>\n </View>\n </View>\n }\n back={\n <View style={{ flex: 1, padding: 22, backgroundColor: '#141416', justifyContent: 'space-between' }}>\n <Text style={{ color: 'rgba(255,255,255,0.55)', fontSize: 11, fontWeight: '700', letterSpacing: 1.2 }}>THIS YEAR</Text>\n {[\n ['Projects shipped', '24'],\n ['Design reviews', '138'],\n ['Mentees', '6'],\n ].map(([k, v]) => (\n <View key={k}>\n <Text style={{ color: '#fff', fontSize: 34, fontWeight: '800', letterSpacing: -1 }}>{v}</Text>\n <Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13 }}>{k}</Text>\n </View>\n ))}\n </View>\n }\n />\n <View style={{ flexDirection: 'row', gap: 10 }}>\n <Pressable accessibilityRole=\"button\" style={{ flex: 1, alignItems: 'center', paddingVertical: 14, borderRadius: 14, backgroundColor: colors.primary }}>\n <Text style={{ color: colors.primaryForeground, fontSize: 15, fontWeight: '600' }}>Message Ana</Text>\n </Pressable>\n <Pressable accessibilityRole=\"button\" style={{ flex: 1, alignItems: 'center', paddingVertical: 14, borderRadius: 14, backgroundColor: colors.muted }}>\n <Text style={{ color: colors.foreground, fontSize: 15, fontWeight: '600' }}>View work</Text>\n </Pressable>\n </View>\n </View>\n );\n}\n"
|
|
119
|
+
"code": "import React, { useState } from 'react';\nimport { Image, Platform, Pressable, Text, View } from 'react-native';\nimport { FlipCard } from '@/components/ui/flip-card';\nimport { useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\n/** Any photo works (`require()` or a URL). */\nconst photo = (id: number, w: number, h: number) => ({ uri: `https://picsum.photos/id/${id}/${w}/${h}` });\n\n/** Smooth darkening toward the bottom of a photo, made from thin bands. */\nfunction Scrim({ height = '55%', max = 0.75 }: { height?: `${number}%` | number; max?: number }) {\n const steps = 40; // fine steps: coarse ones read as faint lines on iOS\n return (\n <View pointerEvents=\"none\" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height }}>\n {Array.from({ length: steps }, (_, i) => (\n <View key={i} style={{ flex: 1, backgroundColor: `rgba(0,0,0,${(max * ((i + 0.5) / steps) ** 1.6).toFixed(3)})` }} />\n ))}\n </View>\n );\n}\n\n/** A screen title, as it sits above the card in your app. */\nfunction Header({ title, sub, right }: { title: string; sub?: string; right?: React.ReactNode }) {\n const { colors } = useKit();\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>\n <View style={{ flex: 1 }}>\n <Text style={{ color: colors.foreground, fontSize: 22, fontWeight: '700', letterSpacing: -0.4 }}>{title}</Text>\n {sub ? <Text style={{ color: colors.mutedForeground, fontSize: 13, marginTop: 2 }}>{sub}</Text> : null}\n </View>\n {right}\n </View>\n );\n}\n\nexport default function Profile() {\n const { colors } = useKit();\n return (\n <View style={{ gap: 20 }}>\n <Header title=\"Design team\" sub=\"Tap a card to see their year\" />\n <FlipCard\n radius={24}\n accessibilityLabel=\"Ana Lima, lead designer\"\n style={{ alignSelf: 'center', width: 250 }}\n front={\n <View style={{ height: 320, backgroundColor: '#222' }}>\n <Image alt=\"\" source={photo(1027, 500, 640)} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} />\n <Scrim />\n <View style={{ flex: 1, justifyContent: 'flex-end', padding: 18 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: '#fff', fontSize: 24, fontWeight: '800', letterSpacing: -0.4 }}>Ana Lima</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: 'rgba(255,255,255,0.8)', fontSize: 13 }}>Lead designer · Lisbon</Text>\n </View>\n </View>\n }\n back={\n <View style={{ flex: 1, padding: 22, backgroundColor: '#141416', justifyContent: 'space-between' }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: 'rgba(255,255,255,0.55)', fontSize: 11, fontWeight: '700', letterSpacing: 1.2 }}>THIS YEAR</Text>\n {[\n ['Projects shipped', '24'],\n ['Design reviews', '138'],\n ['Mentees', '6'],\n ].map(([k, v]) => (\n <View key={k}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: '#fff', fontSize: 34, fontWeight: '800', letterSpacing: -1 }}>{v}</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13 }}>{k}</Text>\n </View>\n ))}\n </View>\n }\n />\n <View style={{ flexDirection: 'row', gap: 10 }}>\n <Pressable accessibilityRole=\"button\" style={{ flex: 1, alignItems: 'center', paddingVertical: 14, borderRadius: 14, backgroundColor: colors.primary }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: colors.primaryForeground, fontSize: 15, fontWeight: '600' }}>Message Ana</Text>\n </Pressable>\n <Pressable accessibilityRole=\"button\" style={{ flex: 1, alignItems: 'center', paddingVertical: 14, borderRadius: 14, backgroundColor: colors.muted }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: colors.foreground, fontSize: 15, fontWeight: '600' }}>View work</Text>\n </Pressable>\n </View>\n </View>\n );\n}\n"
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"name": "Flashcard",
|
|
123
123
|
"title": "Flashcard",
|
|
124
|
-
"code": "import React, { useState } from 'react';\nimport { Image, Platform, Pressable, Text, View } from 'react-native';\nimport { FlipCard } from '@/components/ui/flip-card';\nimport { useKit } from '@/lib/kit';\n\n/** Any photo works (`require()` or a URL). */\nconst photo = (id: number, w: number, h: number) => ({ uri: `https://picsum.photos/id/${id}/${w}/${h}` });\n\n/** Smooth darkening toward the bottom of a photo, made from thin bands. */\nfunction Scrim({ height = '55%', max = 0.75 }: { height?: `${number}%` | number; max?: number }) {\n const steps = 40; // fine steps: coarse ones read as faint lines on iOS\n return (\n <View pointerEvents=\"none\" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height }}>\n {Array.from({ length: steps }, (_, i) => (\n <View key={i} style={{ flex: 1, backgroundColor: `rgba(0,0,0,${(max * ((i + 0.5) / steps) ** 1.6).toFixed(3)})` }} />\n ))}\n </View>\n );\n}\n\n/** A screen title, as it sits above the card in your app. */\nfunction Header({ title, sub, right }: { title: string; sub?: string; right?: React.ReactNode }) {\n const { colors } = useKit();\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>\n <View style={{ flex: 1 }}>\n <Text style={{ color: colors.foreground, fontSize: 22, fontWeight: '700', letterSpacing: -0.4 }}>{title}</Text>\n {sub ? <Text style={{ color: colors.mutedForeground, fontSize: 13, marginTop: 2 }}>{sub}</Text> : null}\n </View>\n {right}\n </View>\n );\n}\n\nexport default function Flashcard() {\n const { colors } = useKit();\n const [flipped, setFlipped] = useState(false);\n return (\n <View style={{ gap: 14 }}>\n <Header\n title=\"Spanish · Fruit\"\n sub=\"12 cards left today\"\n right={<Text style={{ color: colors.mutedForeground, fontSize: 13, fontWeight: '600' }}>8 / 20</Text>}\n />\n <View style={{ height: 6, borderRadius: 3, backgroundColor: colors.muted, marginBottom: 6 }}>\n <View style={{ width: '40%', height: 6, borderRadius: 3, backgroundColor: colors.success }} />\n </View>\n <FlipCard\n radius={22}\n flipped={flipped}\n onFlip={setFlipped}\n accessibilityLabel=\"Flashcard\"\n front={\n <View style={{ height: 240, backgroundColor: '#3a0d10' }}>\n <Image alt=\"\" source={photo(1080, 720, 400)} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} />\n <Scrim max={0.8} />\n <View style={{ flex: 1, justifyContent: 'flex-end', padding: 18 }}>\n <Text style={{ color: 'rgba(255,255,255,0.75)', fontSize: 11, fontWeight: '700', letterSpacing: 1 }}>SPANISH</Text>\n <Text style={{ color: '#fff', fontSize: 30, fontWeight: '800' }}>la fresa</Text>\n </View>\n </View>\n }\n back={\n <View style={{ flex: 1, padding: 20, backgroundColor: colors.card, justifyContent: 'center', gap: 6 }}>\n <Text style={{ color: colors.mutedForeground, fontSize: 11, fontWeight: '700', letterSpacing: 1 }}>ENGLISH</Text>\n <Text style={{ color: colors.foreground, fontSize: 30, fontWeight: '800' }}>the strawberry</Text>\n <Text style={{ color: colors.mutedForeground, fontSize: 14 }}>“Me encantan las fresas con nata.”</Text>\n </View>\n }\n />\n <View style={{ flexDirection: 'row', gap: 10 }}>\n {['Again', 'Good', 'Easy'].map((label) => (\n <Pressable\n key={label}\n onPress={() => setFlipped((f) => !f)}\n accessibilityRole=\"button\"\n style={{ flex: 1, alignItems: 'center', paddingVertical: 12, borderRadius: 12, backgroundColor: colors.muted }}\n >\n <Text style={{ color: colors.foreground, fontSize: 14, fontWeight: '600' }}>{label}</Text>\n </Pressable>\n ))}\n </View>\n </View>\n );\n}\n"
|
|
124
|
+
"code": "import React, { useState } from 'react';\nimport { Image, Platform, Pressable, Text, View } from 'react-native';\nimport { FlipCard } from '@/components/ui/flip-card';\nimport { useKit } from '@/lib/kit';\n\n/** Demo control text (buttons, chips, labels on fixed shapes) stops at iOS's largest standard text size. */\nconst FONT_CAP = 1.35;\n\n/** Any photo works (`require()` or a URL). */\nconst photo = (id: number, w: number, h: number) => ({ uri: `https://picsum.photos/id/${id}/${w}/${h}` });\n\n/** Smooth darkening toward the bottom of a photo, made from thin bands. */\nfunction Scrim({ height = '55%', max = 0.75 }: { height?: `${number}%` | number; max?: number }) {\n const steps = 40; // fine steps: coarse ones read as faint lines on iOS\n return (\n <View pointerEvents=\"none\" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height }}>\n {Array.from({ length: steps }, (_, i) => (\n <View key={i} style={{ flex: 1, backgroundColor: `rgba(0,0,0,${(max * ((i + 0.5) / steps) ** 1.6).toFixed(3)})` }} />\n ))}\n </View>\n );\n}\n\n/** A screen title, as it sits above the card in your app. */\nfunction Header({ title, sub, right }: { title: string; sub?: string; right?: React.ReactNode }) {\n const { colors } = useKit();\n return (\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>\n <View style={{ flex: 1 }}>\n <Text style={{ color: colors.foreground, fontSize: 22, fontWeight: '700', letterSpacing: -0.4 }}>{title}</Text>\n {sub ? <Text style={{ color: colors.mutedForeground, fontSize: 13, marginTop: 2 }}>{sub}</Text> : null}\n </View>\n {right}\n </View>\n );\n}\n\nexport default function Flashcard() {\n const { colors } = useKit();\n const [flipped, setFlipped] = useState(false);\n return (\n <View style={{ gap: 14 }}>\n <Header\n title=\"Spanish · Fruit\"\n sub=\"12 cards left today\"\n right={<Text style={{ color: colors.mutedForeground, fontSize: 13, fontWeight: '600' }}>8 / 20</Text>}\n />\n <View style={{ height: 6, borderRadius: 3, backgroundColor: colors.muted, marginBottom: 6 }}>\n <View style={{ width: '40%', height: 6, borderRadius: 3, backgroundColor: colors.success }} />\n </View>\n <FlipCard\n radius={22}\n flipped={flipped}\n onFlip={setFlipped}\n accessibilityLabel=\"Flashcard\"\n front={\n <View style={{ height: 240, backgroundColor: '#3a0d10' }}>\n <Image alt=\"\" source={photo(1080, 720, 400)} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} />\n <Scrim max={0.8} />\n <View style={{ flex: 1, justifyContent: 'flex-end', padding: 18 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: 'rgba(255,255,255,0.75)', fontSize: 11, fontWeight: '700', letterSpacing: 1 }}>SPANISH</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: '#fff', fontSize: 30, fontWeight: '800' }}>la fresa</Text>\n </View>\n </View>\n }\n back={\n <View style={{ flex: 1, padding: 20, backgroundColor: colors.card, justifyContent: 'center', gap: 6 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: colors.mutedForeground, fontSize: 11, fontWeight: '700', letterSpacing: 1 }}>ENGLISH</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: colors.foreground, fontSize: 30, fontWeight: '800' }}>the strawberry</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: colors.mutedForeground, fontSize: 14 }}>“Me encantan las fresas con nata.”</Text>\n </View>\n }\n />\n <View style={{ flexDirection: 'row', gap: 10 }}>\n {['Again', 'Good', 'Easy'].map((label) => (\n <Pressable\n key={label}\n onPress={() => setFlipped((f) => !f)}\n accessibilityRole=\"button\"\n style={{ flex: 1, alignItems: 'center', paddingVertical: 12, borderRadius: 12, backgroundColor: colors.muted }}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: colors.foreground, fontSize: 14, fontWeight: '600' }}>{label}</Text>\n </Pressable>\n ))}\n </View>\n </View>\n );\n}\n"
|
|
125
125
|
}
|
|
126
126
|
]
|
|
127
127
|
}
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
{
|
|
176
176
|
"name": "Payment",
|
|
177
177
|
"title": "Payment",
|
|
178
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View } from 'react-native';\nimport { FloatingInput } from '@/components/ui/floating-input';\nimport { readableOn, useKit } from '@/lib/kit';\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nfunction Card({ title, subtitle, screen, children }: { title: string; subtitle?: string; screen?: boolean; children: React.ReactNode }) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n { padding: 16, gap: 12 },\n screen\n ? { flex: 1, backgroundColor: colors.background }\n : { borderRadius: radius.xl, borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border, backgroundColor: colors.card },\n ]}\n >\n <View style={{ gap: 2, marginBottom: 2 }}>\n <Text style={{ fontSize: 16, fontWeight: '400', color: colors.foreground }}>{title}</Text>\n {subtitle ? <Text style={{ fontSize: 13, color: colors.mutedForeground }}>{subtitle}</Text> : null}\n </View>\n {children}\n </View>\n );\n}\n\nconst formatCard = (v: string) =>\n v\n .replace(/\\D/g, '')\n .slice(0, 16)\n .replace(/(\\d{4})(?=\\d)/g, '$1 ');\n\nconst formatExpiry = (v: string) => {\n const d = v.replace(/\\D/g, '').slice(0, 4);\n return d.length > 2 ? `${d.slice(0, 2)}/${d.slice(2)}` : d;\n};\n\nconst cardBrand = (v: string) => {\n const d = v.replace(/\\D/g, '');\n if (/^4/.test(d)) return 'VISA';\n if (/^(5[1-5]|2[2-7])/.test(d)) return 'MC';\n if (/^3[47]/.test(d)) return 'AMEX';\n return undefined;\n};\n\n/** '' = not finished yet, otherwise an error message or null when valid. */\nconst expiryError = (v: string): string | null | '' => {\n if (v.length < 5) return '';\n const month = Number(v.slice(0, 2));\n const year = 2000 + Number(v.slice(3));\n if (month < 1 || month > 12) return 'Month must be 01–12';\n const now = new Date();\n if (year < now.getFullYear() || (year === now.getFullYear() && month < now.getMonth() + 1)) return 'This card has expired';\n return null;\n};\n\nconst rupees = (n: number) => {\n const [int, frac] = n.toFixed(2).split('.');\n return `₹${int.replace(/(\\d)(?=(\\d\\d)+\\d$)/g, '$1,')}${frac === '00' ? '' : `.${frac}`}`;\n};\n\nexport default function Payment({ screen }: DemoProps) {\n const { colors, radius } = useKit();\n const [amount, setAmount] = useState('1299');\n const [card, setCard] = useState('4242 4242 4242 4242');\n const [expiry, setExpiry] = useState('');\n const [cvc, setCvc] = useState('');\n const [paid, setPaid] = useState(false);\n const brand = cardBrand(card);\n const cvcLen = brand === 'AMEX' ? 4 : 3;\n const expErr = expiryError(expiry);\n const ready = Number(amount) > 0 && card.length >= 18 && expErr === null && cvc.length === cvcLen;\n return (\n <Card title=\"Add funds\" subtitle=\"Charged securely — you can cancel anytime.\" screen={screen}>\n <FloatingInput label=\"Amount\" prefix=\"₹\" suffix=\"INR\" keyboardType=\"decimal-pad\" value={amount} onChangeText={setAmount} />\n <FloatingInput\n label=\"Card number\"\n keyboardType=\"number-pad\"\n autoComplete=\"cc-number\"\n value={card}\n onChangeText={(v) => setCard(formatCard(v))}\n maxLength={19}\n suffix={brand}\n success={card.length === 19}\n />\n <View style={{ flexDirection: 'row', gap: 10, alignItems: 'flex-start' }}>\n <FloatingInput\n label=\"MM/YY\"\n keyboardType=\"number-pad\"\n autoComplete=\"cc-exp\"\n value={expiry}\n onChangeText={(v) => setExpiry(formatExpiry(v))}\n maxLength={5}\n error={expErr || undefined}\n success={expErr === null}\n style={{ flex: 1 }}\n />\n <FloatingInput\n label=\"CVC\"\n keyboardType=\"number-pad\"\n autoComplete=\"cc-csc\"\n secureTextEntry\n value={cvc}\n onChangeText={(v) => setCvc(v.replace(/\\D/g, '').slice(0, cvcLen))}\n maxLength={cvcLen}\n helper={`${cvcLen} digits on the ${brand === 'AMEX' ? 'front' : 'back'}`}\n style={{ flex: 1 }}\n />\n </View>\n <Pressable\n disabled={!ready}\n onPress={() => setPaid(true)}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled: !ready }}\n style={({ pressed }) => ({\n
|
|
178
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View } from 'react-native';\nimport { FloatingInput } from '@/components/ui/floating-input';\nimport { readableOn, useKit } from '@/lib/kit';\n\n/** Control text stops at iOS's largest standard size. */\nconst FONT_CAP = 1.35;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nfunction Card({ title, subtitle, screen, children }: { title: string; subtitle?: string; screen?: boolean; children: React.ReactNode }) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n { padding: 16, gap: 12 },\n screen\n ? { flex: 1, backgroundColor: colors.background }\n : { borderRadius: radius.xl, borderWidth: StyleSheet.hairlineWidth, borderColor: colors.border, backgroundColor: colors.card },\n ]}\n >\n <View style={{ gap: 2, marginBottom: 2 }}>\n <Text style={{ fontSize: 16, fontWeight: '400', color: colors.foreground }}>{title}</Text>\n {subtitle ? <Text style={{ fontSize: 13, color: colors.mutedForeground }}>{subtitle}</Text> : null}\n </View>\n {children}\n </View>\n );\n}\n\nconst formatCard = (v: string) =>\n v\n .replace(/\\D/g, '')\n .slice(0, 16)\n .replace(/(\\d{4})(?=\\d)/g, '$1 ');\n\nconst formatExpiry = (v: string) => {\n const d = v.replace(/\\D/g, '').slice(0, 4);\n return d.length > 2 ? `${d.slice(0, 2)}/${d.slice(2)}` : d;\n};\n\nconst cardBrand = (v: string) => {\n const d = v.replace(/\\D/g, '');\n if (/^4/.test(d)) return 'VISA';\n if (/^(5[1-5]|2[2-7])/.test(d)) return 'MC';\n if (/^3[47]/.test(d)) return 'AMEX';\n return undefined;\n};\n\n/** '' = not finished yet, otherwise an error message or null when valid. */\nconst expiryError = (v: string): string | null | '' => {\n if (v.length < 5) return '';\n const month = Number(v.slice(0, 2));\n const year = 2000 + Number(v.slice(3));\n if (month < 1 || month > 12) return 'Month must be 01–12';\n const now = new Date();\n if (year < now.getFullYear() || (year === now.getFullYear() && month < now.getMonth() + 1)) return 'This card has expired';\n return null;\n};\n\nconst rupees = (n: number) => {\n const [int, frac] = n.toFixed(2).split('.');\n return `₹${int.replace(/(\\d)(?=(\\d\\d)+\\d$)/g, '$1,')}${frac === '00' ? '' : `.${frac}`}`;\n};\n\nexport default function Payment({ screen }: DemoProps) {\n const { colors, radius } = useKit();\n const [amount, setAmount] = useState('1299');\n const [card, setCard] = useState('4242 4242 4242 4242');\n const [expiry, setExpiry] = useState('');\n const [cvc, setCvc] = useState('');\n const [paid, setPaid] = useState(false);\n const brand = cardBrand(card);\n const cvcLen = brand === 'AMEX' ? 4 : 3;\n const expErr = expiryError(expiry);\n const ready = Number(amount) > 0 && card.length >= 18 && expErr === null && cvc.length === cvcLen;\n return (\n <Card title=\"Add funds\" subtitle=\"Charged securely — you can cancel anytime.\" screen={screen}>\n <FloatingInput label=\"Amount\" prefix=\"₹\" suffix=\"INR\" keyboardType=\"decimal-pad\" value={amount} onChangeText={setAmount} />\n <FloatingInput\n label=\"Card number\"\n keyboardType=\"number-pad\"\n autoComplete=\"cc-number\"\n value={card}\n onChangeText={(v) => setCard(formatCard(v))}\n maxLength={19}\n suffix={brand}\n success={card.length === 19}\n />\n <View style={{ flexDirection: 'row', gap: 10, alignItems: 'flex-start' }}>\n <FloatingInput\n label=\"MM/YY\"\n keyboardType=\"number-pad\"\n autoComplete=\"cc-exp\"\n value={expiry}\n onChangeText={(v) => setExpiry(formatExpiry(v))}\n maxLength={5}\n error={expErr || undefined}\n success={expErr === null}\n style={{ flex: 1 }}\n />\n <FloatingInput\n label=\"CVC\"\n keyboardType=\"number-pad\"\n autoComplete=\"cc-csc\"\n secureTextEntry\n value={cvc}\n onChangeText={(v) => setCvc(v.replace(/\\D/g, '').slice(0, cvcLen))}\n maxLength={cvcLen}\n helper={`${cvcLen} digits on the ${brand === 'AMEX' ? 'front' : 'back'}`}\n style={{ flex: 1 }}\n />\n </View>\n <Pressable\n disabled={!ready}\n onPress={() => setPaid(true)}\n accessibilityRole=\"button\"\n accessibilityState={{ disabled: !ready }}\n style={({ pressed }) => ({\n minHeight: 50,\n marginTop: 4,\n borderRadius: radius.md,\n alignItems: 'center',\n justifyContent: 'center',\n backgroundColor: ready ? colors.accent : colors.muted,\n opacity: pressed ? 0.85 : 1,\n })}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 16, fontWeight: '400', color: ready ? readableOn(colors.accent) : colors.mutedForeground }}>\n {paid ? 'Paid ✓' : `Pay ${rupees(Number(amount) || 0)}`}\n </Text>\n </Pressable>\n </Card>\n );\n}\n"
|
|
179
179
|
},
|
|
180
180
|
{
|
|
181
181
|
"name": "Variants",
|
|
@@ -64,12 +64,12 @@
|
|
|
64
64
|
{
|
|
65
65
|
"name": "Hero",
|
|
66
66
|
"title": "Hero",
|
|
67
|
-
"code": "import { Text, View } from 'react-native';\nimport { GRAIN_PALETTES, GrainGradient } from '@/components/ui/grain';\nimport { useKit } from '@/lib/kit';\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nexport default function Hero({ screen }: DemoProps) {\n const { radius } = useKit();\n return (\n <GrainGradient palette=\"ember\" seed={1} style={screen ? { flex: 1 } : { height: 460, borderRadius: radius.xl }}>\n <View style={{ flex: 1, justifyContent: 'flex-end', padding: 24, gap: 6 }}>\n <Text style={{ color: '#ffffff', fontSize: 34, fontWeight: '700', letterSpacing: -0.8 }}>Golden hour</Text>\n <Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: 15 }}>Warm light, soft grain, no photo needed.</Text>\n </View>\n </GrainGradient>\n );\n}\n"
|
|
67
|
+
"code": "import { Text, View } from 'react-native';\nimport { GRAIN_PALETTES, GrainGradient } from '@/components/ui/grain';\nimport { useKit } from '@/lib/kit';\n\n/** Controls stop at iOS's largest standard text size; content text stays uncapped. */\nconst FONT_CAP = 1.35;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nexport default function Hero({ screen }: DemoProps) {\n const { radius } = useKit();\n return (\n <GrainGradient palette=\"ember\" seed={1} style={screen ? { flex: 1 } : { height: 460, borderRadius: radius.xl }}>\n <View style={{ flex: 1, justifyContent: 'flex-end', padding: 24, gap: 6 }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: '#ffffff', fontSize: 34, fontWeight: '700', letterSpacing: -0.8 }}>Golden hour</Text>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ color: 'rgba(255,255,255,0.8)', fontSize: 15 }}>Warm light, soft grain, no photo needed.</Text>\n </View>\n </GrainGradient>\n );\n}\n"
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"name": "Palettes",
|
|
71
71
|
"title": "Palettes",
|
|
72
|
-
"code": "import { Text, View } from 'react-native';\nimport { GRAIN_PALETTES, GrainGradient } from '@/components/ui/grain';\nimport { useKit } from '@/lib/kit';\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nexport default function Palettes({ screen }: DemoProps) {\n const { radius } = useKit();\n return (\n <View style={[{ flexDirection: 'row', flexWrap: 'wrap', gap: 12 }, screen && { padding: 16 }]}>\n {GRAIN_PALETTES.map((p, i) => (\n <GrainGradient key={p} palette={p} seed={i} style={{ width: '47%', flexGrow: 1, height: 120, borderRadius: radius.lg }}>\n <Text style={{ margin: 12, color: ['peach', 'mint', 'lavender'].includes(p) ? '#1d1a26' : '#ffffff', fontWeight: '600', textTransform: 'capitalize' }}>{p}</Text>\n </GrainGradient>\n ))}\n </View>\n );\n}\n"
|
|
72
|
+
"code": "import { Text, View } from 'react-native';\nimport { GRAIN_PALETTES, GrainGradient } from '@/components/ui/grain';\nimport { useKit } from '@/lib/kit';\n\n/** Controls stop at iOS's largest standard text size; content text stays uncapped. */\nconst FONT_CAP = 1.35;\n\n/** From the Expo Go viewer: `screen` = this IS the phone screen, so no mock frame; `bottomInset` = home indicator. */\ntype DemoProps = { screen?: boolean; bottomInset?: number };\n\nexport default function Palettes({ screen }: DemoProps) {\n const { radius } = useKit();\n return (\n <View style={[{ flexDirection: 'row', flexWrap: 'wrap', gap: 12 }, screen && { padding: 16 }]}>\n {GRAIN_PALETTES.map((p, i) => (\n <GrainGradient key={p} palette={p} seed={i} style={{ width: '47%', flexGrow: 1, height: 120, borderRadius: radius.lg }}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ margin: 12, color: ['peach', 'mint', 'lavender'].includes(p) ? '#1d1a26' : '#ffffff', fontWeight: '600', textTransform: 'capitalize' }}>{p}</Text>\n </GrainGradient>\n ))}\n </View>\n );\n}\n"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"name": "Custom",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
{
|
|
25
25
|
"name": "Preview",
|
|
26
26
|
"title": "Preview",
|
|
27
|
-
"code": "import React, { useEffect, useRef, useState } from 'react';\nimport { Animated, Image, Platform, Pressable, ScrollView, StyleSheet, Text, View, type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native';\nimport { BlurView } from 'expo-blur';\nimport Svg, { Circle, Path } from 'react-native-svg';\nimport { useKit } from '@/lib/kit';\n\ntype TabKey = 'index' | 'explore' | 'alerts' | 'profile';\n\nconst TABS: { key: TabKey; title: string; badge?: string; icon: (c: string) => React.ReactNode }[] = [\n { key: 'index', title: 'Home', icon: (c) => <Path d=\"M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z\" stroke={c} strokeWidth={2} strokeLinejoin=\"round\" fill=\"none\" /> },\n {\n key: 'explore',\n title: 'Explore',\n icon: (c) => (\n <>\n <Circle cx={12} cy={12} r={9} stroke={c} strokeWidth={2} fill=\"none\" />\n <Path d=\"m15.5 8.5-2 5-5 2 2-5z\" stroke={c} strokeWidth={2} strokeLinejoin=\"round\" fill=\"none\" />\n </>\n ),\n },\n { key: 'alerts', title: 'Alerts', badge: '3', icon: (c) => <Path d=\"M6 8a6 6 0 1 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21h4\" stroke={c} strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" fill=\"none\" /> },\n {\n key: 'profile',\n title: 'Profile',\n icon: (c) => (\n <>\n <Circle cx={12} cy={8} r={4} stroke={c} strokeWidth={2} fill=\"none\" />\n <Path d=\"M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6\" stroke={c} strokeWidth={2} strokeLinecap=\"round\" fill=\"none\" />\n </>\n ),\n },\n];\n\nconst PHOTOS = [\n 'photo-1501785888041-af3ef285b470',\n 'photo-1469474968028-56623f02e42e',\n 'photo-1500530855697-b586d89ba3ee',\n 'photo-1470071459604-3b5ec3a7fe05',\n 'photo-1441974231531-c6227db76b6e',\n].map((id) => `https://images.unsplash.com/${id}?w=700&auto=format&fit=crop`);\n\nconst COPY: Record<TabKey, { title: string; sub: string }> = {\n index: { title: 'For you', sub: 'Places picked for this weekend' },\n explore: { title: 'Explore', sub: 'Trending trails near you' },\n alerts: { title: 'Alerts', sub: '3 new since yesterday' },\n profile: { title: 'Profile', sub: 'Trips, saves and settings' },\n};\n\nconst BAR_H = 64;\n\nfunction GlassBar({\n value,\n onChange,\n dark,\n collapsed,\n onExpand,\n}: {\n value: TabKey;\n onChange: (k: TabKey) => void;\n dark: boolean;\n collapsed: boolean;\n onExpand: () => void;\n}) {\n const [w, setW] = useState(0);\n // 0 = full bar, 1 = folded into a circle (JS driver: it animates the bar's width).\n const fold = useRef(new Animated.Value(0)).current;\n useEffect(() => {\n Animated.spring(fold, { toValue: collapsed ? 1 : 0, useNativeDriver: false, damping: 20, stiffness: 220, mass: 0.9 }).start();\n }, [collapsed, fold]);\n const width = fold.interpolate({ inputRange: [0, 1], outputRange: [w || 1, BAR_H] });\n const rowOpacity = fold.interpolate({ inputRange: [0, 0.45], outputRange: [1, 0], extrapolate: 'clamp' });\n const dotOpacity = fold.interpolate({ inputRange: [0.55, 1], outputRange: [0, 1], extrapolate: 'clamp' });\n const dotScale = fold.interpolate({ inputRange: [0.55, 1], outputRange: [0.6, 1], extrapolate: 'clamp' });\n const active = TABS.find((t) => t.key === value) ?? TABS[0];\n const x = useRef(new Animated.Value(0)).current;\n const stretch = useRef(new Animated.Value(1)).current;\n const index = TABS.findIndex((t) => t.key === value);\n const itemW = w ? (w - 8) / TABS.length : 0;\n const tint = dark ? '#0a84ff' : '#007aff';\n const inactive = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';\n\n useEffect(() => {\n if (!itemW) return;\n Animated.parallel([\n Animated.spring(x, { toValue: index * itemW, useNativeDriver: true, damping: 18, stiffness: 220, mass: 0.9 }),\n Animated.sequence([\n Animated.timing(stretch, { toValue: 1.22, duration: 110, useNativeDriver: true }),\n Animated.spring(stretch, { toValue: 1, useNativeDriver: true, damping: 10, stiffness: 260 }),\n ]),\n ]).start();\n }, [index, itemW, x, stretch]);\n\n return (\n <View pointerEvents=\"box-none\" style={styles.host} onLayout={(e: LayoutChangeEvent) => setW(Math.round(e.nativeEvent.layout.width))}>\n <Animated.View style={[styles.shadow, dark ? styles.shadowDark : styles.shadowLight, w ? { width } : null]}>\n <View style={[styles.glass, { borderColor: dark ? 'rgba(255,255,255,0.14)' : 'rgba(255,255,255,0.8)' }]}>\n <BlurView intensity={Platform.OS === 'ios' ? 70 : 40} tint={dark ? 'dark' : 'light'} style={[StyleSheet.absoluteFill, styles.round]} />\n <View style={[StyleSheet.absoluteFill, { backgroundColor: dark ? 'rgba(28,28,34,0.5)' : 'rgba(255,255,255,0.55)' }]} />\n <View style={[styles.rim, { backgroundColor: dark ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.95)' }]} />\n {itemW ? (\n <Animated.View\n style={[\n styles.bubble,\n {\n width: itemW,\n backgroundColor: dark ? 'rgba(255,255,255,0.14)' : 'rgba(0,0,0,0.06)',\n transform: [{ translateX: x }, { scaleX: stretch }, { scaleY: stretch.interpolate({ inputRange: [1, 1.22], outputRange: [1, 0.88] }) }],\n },\n ]}\n />\n ) : null}\n <Animated.View pointerEvents={collapsed ? 'none' : 'auto'} style={[styles.row, w ? { width: w } : null, { opacity: rowOpacity }]}>\n {TABS.map((t) => {\n const on = t.key === value;\n const c = on ? tint : inactive;\n return (\n <Pressable\n key={t.key}\n onPress={() => onChange(t.key)}\n accessibilityRole=\"tab\"\n accessibilityState={{ selected: on }}\n accessibilityLabel={t.title}\n style={styles.item}\n >\n <View>\n <Svg width={24} height={24} viewBox=\"0 0 24 24\">\n {t.icon(c)}\n </Svg>\n {t.badge ? (\n <View style={styles.badge}>\n <Text style={styles.badgeText}>{t.badge}</Text>\n </View>\n ) : null}\n </View>\n <Text style={[styles.label, { color: c }]}>{t.title}</Text>\n </Pressable>\n );\n })}\n </Animated.View>\n <Animated.View pointerEvents={collapsed ? 'auto' : 'none'} style={[styles.dot, { opacity: dotOpacity, transform: [{ scale: dotScale }] }]}>\n <Pressable onPress={onExpand} accessibilityRole=\"button\" accessibilityLabel={`${active.title}. Show tab bar`} style={styles.dotPress}>\n <Svg width={26} height={26} viewBox=\"0 0 24 24\">\n {active.icon(tint)}\n </Svg>\n </Pressable>\n </Animated.View>\n </View>\n </Animated.View>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n root: { flex: 1 },\n feed: { paddingHorizontal: 16, paddingTop: 12, paddingBottom: 120, gap: 12 },\n large: { fontSize: 32, lineHeight: 38, letterSpacing: -0.8, fontWeight: '400' },\n sub: { fontSize: 15, marginTop: -6, marginBottom: 6 },\n card: { height: 190, borderRadius: 22, overflow: 'hidden', justifyContent: 'flex-end', padding: 16 },\n cardShade: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.18)' },\n cardTitle: { color: '#ffffff', fontSize: 20, fontWeight: '400', letterSpacing: -0.3 },\n host: { position: 'absolute', left: 20, right: 20, bottom: 24 },\n shadow: { height: BAR_H, borderRadius: BAR_H / 2 },\n shadowLight: { boxShadow: '0 10px 28px -8px rgba(0,0,0,0.22)' },\n shadowDark: { boxShadow: '0 10px 28px -8px rgba(0,0,0,0.7)' },\n glass: { flex: 1, borderRadius: 32, overflow: 'hidden', borderWidth: StyleSheet.hairlineWidth * 2 },\n round: { borderRadius: 32 },\n rim: { position: 'absolute', top: 0, left: 24, right: 24, height: 1 },\n bubble: { position: 'absolute', top: 4, bottom: 4, left: 4, borderRadius: 28 },\n row: { flex: 1, flexDirection: 'row', paddingHorizontal: 4 },\n dot: { position: 'absolute', top: 0, left: 0, width: BAR_H, height: BAR_H },\n dotPress: { flex: 1, alignItems: 'center', justifyContent: 'center' },\n item: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 2 },\n label: { fontSize: 10.5, fontWeight: '400' },\n badge: { position: 'absolute', top: -4, right: -10, minWidth: 17, height: 17, borderRadius: 9, paddingHorizontal: 4, backgroundColor: '#ff3b30', alignItems: 'center', justifyContent: 'center' },\n badgeText: { color: '#ffffff', fontSize: 10.5, fontWeight: '400' },\n});\n\nexport default function Preview() {\n const { dark } = useKit();\n const [tab, setTab] = useState<TabKey>('index');\n const [collapsed, setCollapsed] = useState(false);\n const lastY = useRef(0);\n const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {\n const y = e.nativeEvent.contentOffset.y;\n const dy = y - lastY.current;\n lastY.current = y;\n if (y < 24) setCollapsed(false);\n else if (dy > 6) setCollapsed(true);\n else if (dy < -6) setCollapsed(false);\n };\n const fg = dark ? '#ffffff' : '#1c1c1e';\n const sub = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';\n const offset = TABS.findIndex((t) => t.key === tab);\n const copy = COPY[tab];\n\n return (\n <View style={[styles.root, { backgroundColor: dark ? '#000000' : '#f2f2f7' }]}>\n <ScrollView contentContainerStyle={styles.feed} showsVerticalScrollIndicator={false} onScroll={onScroll} scrollEventThrottle={16}>\n <Text style={[styles.large, { color: fg }]}>{copy.title}</Text>\n <Text style={[styles.sub, { color: sub }]}>{copy.sub}</Text>\n {[0, 1, 2, 3, 4].map((i) => {\n const p = PHOTOS[(i + offset) % PHOTOS.length];\n return (\n <View key={`${tab}-${i}`} style={styles.card}>\n <Image source={{ uri: p }} alt=\"\" style={StyleSheet.absoluteFill} resizeMode=\"cover\" />\n <View style={styles.cardShade} />\n <Text style={styles.cardTitle}>{['Lake Braies', 'Dolomites', 'Valley trail', 'Blue ridge', 'Old forest'][(i + offset) % 5]}</Text>\n </View>\n );\n })}\n </ScrollView>\n <GlassBar value={tab} onChange={setTab} dark={dark} collapsed={collapsed} onExpand={() => setCollapsed(false)} />\n </View>\n );\n}\n"
|
|
27
|
+
"code": "import React, { useEffect, useRef, useState } from 'react';\nimport { Animated, Image, Platform, Pressable, ScrollView, StyleSheet, Text, View, type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native';\nimport { BlurView } from 'expo-blur';\nimport Svg, { Circle, Path } from 'react-native-svg';\nimport { useKit } from '@/lib/kit';\n\n/** Control text stops at iOS's largest standard size. */\nconst FONT_CAP = 1.35;\n\ntype TabKey = 'index' | 'explore' | 'alerts' | 'profile';\n\nconst TABS: { key: TabKey; title: string; badge?: string; icon: (c: string) => React.ReactNode }[] = [\n { key: 'index', title: 'Home', icon: (c) => <Path d=\"M3 10.5 12 3l9 7.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z\" stroke={c} strokeWidth={2} strokeLinejoin=\"round\" fill=\"none\" /> },\n {\n key: 'explore',\n title: 'Explore',\n icon: (c) => (\n <>\n <Circle cx={12} cy={12} r={9} stroke={c} strokeWidth={2} fill=\"none\" />\n <Path d=\"m15.5 8.5-2 5-5 2 2-5z\" stroke={c} strokeWidth={2} strokeLinejoin=\"round\" fill=\"none\" />\n </>\n ),\n },\n { key: 'alerts', title: 'Alerts', badge: '3', icon: (c) => <Path d=\"M6 8a6 6 0 1 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21h4\" stroke={c} strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" fill=\"none\" /> },\n {\n key: 'profile',\n title: 'Profile',\n icon: (c) => (\n <>\n <Circle cx={12} cy={8} r={4} stroke={c} strokeWidth={2} fill=\"none\" />\n <Path d=\"M4 21c1.5-4 4.5-6 8-6s6.5 2 8 6\" stroke={c} strokeWidth={2} strokeLinecap=\"round\" fill=\"none\" />\n </>\n ),\n },\n];\n\nconst PHOTOS = [\n 'photo-1501785888041-af3ef285b470',\n 'photo-1469474968028-56623f02e42e',\n 'photo-1500530855697-b586d89ba3ee',\n 'photo-1470071459604-3b5ec3a7fe05',\n 'photo-1441974231531-c6227db76b6e',\n].map((id) => `https://images.unsplash.com/${id}?w=700&auto=format&fit=crop`);\n\nconst COPY: Record<TabKey, { title: string; sub: string }> = {\n index: { title: 'For you', sub: 'Places picked for this weekend' },\n explore: { title: 'Explore', sub: 'Trending trails near you' },\n alerts: { title: 'Alerts', sub: '3 new since yesterday' },\n profile: { title: 'Profile', sub: 'Trips, saves and settings' },\n};\n\nconst BAR_H = 64;\n\nfunction GlassBar({\n value,\n onChange,\n dark,\n collapsed,\n onExpand,\n}: {\n value: TabKey;\n onChange: (k: TabKey) => void;\n dark: boolean;\n collapsed: boolean;\n onExpand: () => void;\n}) {\n const [w, setW] = useState(0);\n // 0 = full bar, 1 = folded into a circle (JS driver: it animates the bar's width).\n const fold = useRef(new Animated.Value(0)).current;\n useEffect(() => {\n Animated.spring(fold, { toValue: collapsed ? 1 : 0, useNativeDriver: false, damping: 29, stiffness: 220, mass: 0.9 }).start();\n }, [collapsed, fold]);\n const width = fold.interpolate({ inputRange: [0, 1], outputRange: [w || 1, BAR_H] });\n const rowOpacity = fold.interpolate({ inputRange: [0, 0.45], outputRange: [1, 0], extrapolate: 'clamp' });\n const dotOpacity = fold.interpolate({ inputRange: [0.55, 1], outputRange: [0, 1], extrapolate: 'clamp' });\n const dotScale = fold.interpolate({ inputRange: [0.55, 1], outputRange: [0.6, 1], extrapolate: 'clamp' });\n const active = TABS.find((t) => t.key === value) ?? TABS[0];\n const x = useRef(new Animated.Value(0)).current;\n const stretch = useRef(new Animated.Value(1)).current;\n const index = TABS.findIndex((t) => t.key === value);\n const itemW = w ? (w - 8) / TABS.length : 0;\n const tint = dark ? '#0a84ff' : '#007aff';\n const inactive = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';\n\n useEffect(() => {\n if (!itemW) return;\n Animated.parallel([\n Animated.spring(x, { toValue: index * itemW, useNativeDriver: true, damping: 29, stiffness: 220, mass: 0.9 }),\n Animated.sequence([\n Animated.timing(stretch, { toValue: 1.22, duration: 110, useNativeDriver: true }),\n Animated.spring(stretch, { toValue: 1, useNativeDriver: true, damping: 33, stiffness: 260 }),\n ]),\n ]).start();\n }, [index, itemW, x, stretch]);\n\n return (\n <View pointerEvents=\"box-none\" style={styles.host} onLayout={(e: LayoutChangeEvent) => setW(Math.round(e.nativeEvent.layout.width))}>\n <Animated.View style={[styles.shadow, dark ? styles.shadowDark : styles.shadowLight, w ? { width } : null]}>\n <View style={[styles.glass, { borderColor: dark ? 'rgba(255,255,255,0.14)' : 'rgba(255,255,255,0.8)' }]}>\n <BlurView intensity={Platform.OS === 'ios' ? 70 : 40} tint={dark ? 'dark' : 'light'} style={[StyleSheet.absoluteFill, styles.round]} />\n <View style={[StyleSheet.absoluteFill, { backgroundColor: dark ? 'rgba(28,28,34,0.5)' : 'rgba(255,255,255,0.55)' }]} />\n <View style={[styles.rim, { backgroundColor: dark ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.95)' }]} />\n {itemW ? (\n <Animated.View\n style={[\n styles.bubble,\n {\n width: itemW,\n backgroundColor: dark ? 'rgba(255,255,255,0.14)' : 'rgba(0,0,0,0.06)',\n transform: [{ translateX: x }, { scaleX: stretch }, { scaleY: stretch.interpolate({ inputRange: [1, 1.22], outputRange: [1, 0.88] }) }],\n },\n ]}\n />\n ) : null}\n <Animated.View pointerEvents={collapsed ? 'none' : 'auto'} style={[styles.row, w ? { width: w } : null, { opacity: rowOpacity }]}>\n {TABS.map((t) => {\n const on = t.key === value;\n const c = on ? tint : inactive;\n return (\n <Pressable\n key={t.key}\n onPress={() => onChange(t.key)}\n accessibilityRole=\"tab\"\n accessibilityState={{ selected: on }}\n accessibilityLabel={t.title}\n style={styles.item}\n >\n <View>\n <Svg width={24} height={24} viewBox=\"0 0 24 24\">\n {t.icon(c)}\n </Svg>\n {t.badge ? (\n <View style={styles.badge}>\n <Text maxFontSizeMultiplier={FONT_CAP} style={styles.badgeText}>{t.badge}</Text>\n </View>\n ) : null}\n </View>\n <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.label, { color: c }]}>{t.title}</Text>\n </Pressable>\n );\n })}\n </Animated.View>\n <Animated.View pointerEvents={collapsed ? 'auto' : 'none'} style={[styles.dot, { opacity: dotOpacity, transform: [{ scale: dotScale }] }]}>\n <Pressable onPress={onExpand} accessibilityRole=\"button\" accessibilityLabel={`${active.title}. Show tab bar`} style={styles.dotPress}>\n <Svg width={26} height={26} viewBox=\"0 0 24 24\">\n {active.icon(tint)}\n </Svg>\n </Pressable>\n </Animated.View>\n </View>\n </Animated.View>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n root: { flex: 1 },\n feed: { paddingHorizontal: 16, paddingTop: 12, paddingBottom: 120, gap: 12 },\n large: { fontSize: 32, lineHeight: 38, letterSpacing: -0.8, fontWeight: '400' },\n sub: { fontSize: 15, marginTop: -6, marginBottom: 6 },\n card: { height: 190, borderRadius: 22, overflow: 'hidden', justifyContent: 'flex-end', padding: 16 },\n cardShade: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.18)' },\n cardTitle: { color: '#ffffff', fontSize: 20, fontWeight: '400', letterSpacing: -0.3 },\n host: { position: 'absolute', left: 20, right: 20, bottom: 24 },\n shadow: { height: BAR_H, borderRadius: BAR_H / 2 },\n shadowLight: { boxShadow: '0 10px 28px -8px rgba(0,0,0,0.22)' },\n shadowDark: { boxShadow: '0 10px 28px -8px rgba(0,0,0,0.7)' },\n glass: { flex: 1, borderRadius: 32, overflow: 'hidden', borderWidth: StyleSheet.hairlineWidth * 2 },\n round: { borderRadius: 32 },\n rim: { position: 'absolute', top: 0, left: 24, right: 24, height: 1 },\n bubble: { position: 'absolute', top: 4, bottom: 4, left: 4, borderRadius: 28 },\n row: { flex: 1, flexDirection: 'row', paddingHorizontal: 4 },\n dot: { position: 'absolute', top: 0, left: 0, width: BAR_H, height: BAR_H },\n dotPress: { flex: 1, alignItems: 'center', justifyContent: 'center' },\n item: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 2 },\n label: { fontSize: 10.5, fontWeight: '400' },\n badge: { position: 'absolute', top: -4, right: -10, minWidth: 17, minHeight: 17, borderRadius: 9, paddingHorizontal: 4, backgroundColor: '#ff3b30', alignItems: 'center', justifyContent: 'center' },\n badgeText: { color: '#ffffff', fontSize: 10.5, fontWeight: '400' },\n});\n\nexport default function Preview() {\n const { dark } = useKit();\n const [tab, setTab] = useState<TabKey>('index');\n const [collapsed, setCollapsed] = useState(false);\n const lastY = useRef(0);\n const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {\n const y = e.nativeEvent.contentOffset.y;\n const dy = y - lastY.current;\n lastY.current = y;\n if (y < 24) setCollapsed(false);\n else if (dy > 6) setCollapsed(true);\n else if (dy < -6) setCollapsed(false);\n };\n const fg = dark ? '#ffffff' : '#1c1c1e';\n const sub = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';\n const offset = TABS.findIndex((t) => t.key === tab);\n const copy = COPY[tab];\n\n return (\n <View style={[styles.root, { backgroundColor: dark ? '#000000' : '#f2f2f7' }]}>\n <ScrollView contentContainerStyle={styles.feed} showsVerticalScrollIndicator={false} onScroll={onScroll} scrollEventThrottle={16}>\n <Text style={[styles.large, { color: fg }]}>{copy.title}</Text>\n <Text style={[styles.sub, { color: sub }]}>{copy.sub}</Text>\n {[0, 1, 2, 3, 4].map((i) => {\n const p = PHOTOS[(i + offset) % PHOTOS.length];\n return (\n <View key={`${tab}-${i}`} style={styles.card}>\n <Image source={{ uri: p }} alt=\"\" style={StyleSheet.absoluteFill} resizeMode=\"cover\" />\n <View style={styles.cardShade} />\n <Text maxFontSizeMultiplier={FONT_CAP} style={styles.cardTitle}>{['Lake Braies', 'Dolomites', 'Valley trail', 'Blue ridge', 'Old forest'][(i + offset) % 5]}</Text>\n </View>\n );\n })}\n </ScrollView>\n <GlassBar value={tab} onChange={setTab} dark={dark} collapsed={collapsed} onExpand={() => setCollapsed(false)} />\n </View>\n );\n}\n"
|
|
28
28
|
}
|
|
29
29
|
]
|
|
30
30
|
}
|