@expokit/cli 0.3.0 → 0.4.0
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/README.md +8 -4
- package/bin/expo-kit.js +137 -24
- package/bin/lib.mjs +110 -1
- package/bin/mcp.mjs +40 -18
- package/package.json +2 -2
- package/registry/_docs/assistant-app.json +47 -0
- package/registry/_docs/avatar.json +1 -1
- package/registry/_docs/badge.json +1 -1
- package/registry/_docs/bottom-nav.json +6 -6
- package/registry/_docs/button-3d.json +2 -2
- package/registry/_docs/calendar.json +5 -5
- package/registry/_docs/checkout.json +7 -1
- package/registry/_docs/confetti.json +3 -3
- package/registry/_docs/date-picker.json +4 -4
- package/registry/_docs/donut-shop.json +88 -0
- package/registry/_docs/drawer.json +4 -4
- package/registry/_docs/fintech-app.json +49 -0
- package/registry/_docs/floating-input.json +5 -5
- package/registry/_docs/food-app.json +47 -0
- package/registry/_docs/glass.json +1 -1
- package/registry/_docs/grain.json +80 -0
- package/registry/_docs/morphing-button-group.json +96 -0
- package/registry/_docs/orb-reveal.json +132 -0
- package/registry/_docs/otp-input.json +4 -4
- package/registry/_docs/password-input.json +3 -3
- package/registry/_docs/qr-code-button.json +122 -0
- package/registry/_docs/search-bar.json +3 -3
- package/registry/_docs/select.json +7 -7
- package/registry/_docs/settings.json +6 -0
- package/registry/_docs/skeleton.json +2 -2
- package/registry/_docs/slider.json +6 -6
- package/registry/_docs/spinner.json +1 -1
- package/registry/_docs/stepper.json +7 -7
- package/registry/_docs/textarea.json +2 -2
- package/registry/_docs/ticket-shape.json +1 -1
- package/registry/_docs/tilt-card.json +4 -4
- package/registry/_docs/time-picker.json +6 -6
- package/registry/_docs/timeline.json +2 -2
- package/registry/_docs/toast.json +8 -8
- package/registry/accordion/files/src/components/ui/accordion.tsx +5 -4
- package/registry/accordion/files/src/lib/kit.tsx +8 -5
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +9 -8
- package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/alert/files/src/components/ui/alert.tsx +7 -4
- package/registry/alert/files/src/components/ui/button.tsx +4 -3
- package/registry/alert/files/src/lib/kit.tsx +8 -5
- package/registry/assistant-app.json +49 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
- package/registry/avatar/files/src/lib/kit.tsx +8 -5
- package/registry/badge/files/src/components/ui/badge.tsx +21 -7
- package/registry/badge/files/src/lib/kit.tsx +8 -5
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +14 -7
- package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
- package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/button/files/src/components/ui/button.tsx +4 -3
- package/registry/button/files/src/lib/kit.tsx +8 -5
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +3 -2
- package/registry/button-3d/files/src/lib/kit.tsx +8 -5
- package/registry/calendar/files/src/components/ui/calendar.tsx +9 -5
- package/registry/calendar/files/src/lib/kit.tsx +8 -5
- package/registry/card/files/src/components/ui/card.tsx +8 -6
- package/registry/card/files/src/lib/kit.tsx +8 -5
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +6 -5
- package/registry/checkbox/files/src/lib/kit.tsx +8 -5
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +4 -2
- package/registry/chip-group/files/src/lib/kit.tsx +8 -5
- package/registry/confetti/files/src/lib/kit.tsx +8 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +23 -20
- package/registry/coupon/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/coupon/files/src/lib/kit.tsx +8 -5
- package/registry/date-picker/files/src/components/ui/calendar.tsx +9 -5
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +2 -2
- package/registry/date-picker/files/src/lib/kit.tsx +8 -5
- package/registry/dialog/files/src/components/ui/dialog.tsx +12 -6
- package/registry/dialog/files/src/lib/kit.tsx +8 -5
- package/registry/donut-shop.json +18 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +25 -19
- package/registry/drawer/files/src/lib/kit.tsx +8 -5
- package/registry/fintech-app.json +57 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/flip-card/files/src/lib/kit.tsx +8 -5
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +16 -6
- package/registry/floating-input/files/src/lib/kit.tsx +8 -5
- package/registry/food-app.json +55 -0
- package/registry/grain/files/src/components/ui/grain.tsx +113 -0
- package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
- package/registry/grain.json +17 -0
- package/registry/kit/files/src/lib/kit.tsx +8 -5
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
- package/registry/list-item/files/src/components/ui/list-item.tsx +20 -16
- package/registry/list-item/files/src/lib/kit.tsx +8 -5
- package/registry/morphing-button-group.json +18 -0
- package/registry/orb-reveal.json +20 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +8 -6
- package/registry/otp-input/files/src/lib/kit.tsx +8 -5
- package/registry/password-input/files/src/components/ui/floating-input.tsx +16 -6
- package/registry/password-input/files/src/components/ui/password-input.tsx +2 -2
- package/registry/password-input/files/src/lib/kit.tsx +8 -5
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +27 -14
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/payment-card/files/src/lib/kit.tsx +8 -5
- package/registry/progress/files/src/components/ui/progress.tsx +8 -5
- package/registry/progress/files/src/lib/kit.tsx +8 -5
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +15 -11
- package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
- package/registry/qr-code-button.json +18 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +10 -9
- package/registry/radio-group/files/src/lib/kit.tsx +8 -5
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +13 -11
- package/registry/search-bar/files/src/lib/kit.tsx +8 -5
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +4 -2
- package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
- package/registry/select/files/src/components/ui/select.tsx +3 -3
- package/registry/select/files/src/lib/kit.tsx +8 -5
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
- package/registry/skeleton/files/src/lib/kit.tsx +8 -5
- package/registry/slider/files/src/components/ui/slider.tsx +5 -4
- package/registry/slider/files/src/lib/kit.tsx +8 -5
- package/registry/spinner/files/src/components/ui/spinner.tsx +17 -12
- package/registry/spinner/files/src/lib/kit.tsx +8 -5
- package/registry/stateful-button/files/src/lib/kit.tsx +8 -5
- package/registry/stepper/files/src/components/ui/stepper.tsx +8 -4
- package/registry/stepper/files/src/lib/kit.tsx +8 -5
- package/registry/switch/files/src/components/ui/switch.tsx +4 -3
- package/registry/switch/files/src/lib/kit.tsx +8 -5
- package/registry/tabs/files/src/components/ui/tabs.tsx +10 -8
- package/registry/tabs/files/src/lib/kit.tsx +8 -5
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +4 -4
- package/registry/text-animation/files/src/lib/kit.tsx +8 -5
- package/registry/textarea/files/src/components/ui/textarea.tsx +11 -9
- package/registry/textarea/files/src/lib/kit.tsx +8 -5
- package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +2 -2
- package/registry/time-picker/files/src/lib/kit.tsx +8 -5
- package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
- package/registry/timeline/files/src/components/ui/timeline.tsx +12 -10
- package/registry/timeline/files/src/lib/kit.tsx +8 -5
- package/registry/toast/files/src/components/ui/button.tsx +4 -3
- package/registry/toast/files/src/components/ui/toast.tsx +13 -11
- package/registry/toast/files/src/lib/kit.tsx +8 -5
- package/schema/components.schema.json +1 -1
- package/registry/_docs/color-swatch.json +0 -82
- package/registry/_docs/kpi-card.json +0 -14
- package/registry/_docs/like-button.json +0 -92
- package/registry/_docs/morph-button.json +0 -128
- package/registry/_docs/pagination-dots.json +0 -108
- package/registry/_docs/shimmer-button.json +0 -98
- package/registry/_docs/tag-input.json +0 -148
- package/registry/color-swatch.json +0 -17
- package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
- package/registry/kpi-card.json +0 -14
- package/registry/like-button.json +0 -18
- package/registry/morph-button.json +0 -18
- package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
- package/registry/pagination-dots/files/src/lib/kit.tsx +0 -219
- package/registry/pagination-dots.json +0 -15
- package/registry/shimmer-button.json +0 -17
- package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
- package/registry/tag-input.json +0 -17
|
@@ -248,22 +248,22 @@
|
|
|
248
248
|
{
|
|
249
249
|
"name": "Birthday",
|
|
250
250
|
"title": "Birthday",
|
|
251
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type TextProps, type ViewProps } from 'react-native';\nimport { DatePicker, type DatePickerPresentation, type DateRange } from '@/components/ui/date-picker';\nimport { useKit } from '@/lib/kit';\n\n/** Demo card that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Card({ style, ...rest }: ViewProps) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n s.card,\n { borderRadius: radius.xl, borderColor: colors.border, backgroundColor: colors.card },\n style,\n ]}\n {...rest}\n />\n );\n}\n\nconst today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate());\n\ntype Opens = Exclude<DatePickerPresentation, 'auto'>;\n\nconst OPENS: { key: Opens; label: string }[] = [\n { key: 'sheet', label: 'Sheet' },\n { key: 'dialog', label: 'Modal' },\n { key: 'popover', label: 'Popover' },\n];\n\n/** Card header with a compact switch for how the picker opens. */\nfunction Header({ title, opens, onOpens }: { title: string; opens: Opens; onOpens: (o: Opens) => void }) {\n const { colors, radius } = useKit();\n return (\n <View style={s.header}>\n <Text style={[s.title, { color: colors.foreground }]} numberOfLines={1}>\n {title}\n </Text>\n <View style={[s.switch, { backgroundColor: colors.muted, borderRadius: radius.md }]} accessibilityRole=\"radiogroup\">\n {OPENS.map((o) => {\n const on = o.key === opens;\n return (\n <Pressable\n key={o.key}\n onPress={() => onOpens(o.key)}\n accessibilityRole=\"radio\"\n accessibilityState={{ checked: on }}\n accessibilityLabel={`Open as ${o.label}`}\n style={[s.switchItem, { borderRadius: Math.max(0, radius.md - 3) }, on && { backgroundColor: colors.background }]}\n >\n <Text style={[s.switchText, { color: on ? colors.foreground : colors.mutedForeground }]}>{o.label}</Text>\n </Pressable>\n );\n })}\n </View>\n </View>\n );\n}\n\nconst s = StyleSheet.create({\n card: { padding: 16, borderWidth: StyleSheet.hairlineWidth
|
|
251
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type TextProps, type ViewProps } from 'react-native';\nimport { DatePicker, type DatePickerPresentation, type DateRange } from '@/components/ui/date-picker';\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\n/** Demo card that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Card({ screen, style, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n s.card,\n screen\n ? { flex: 1, backgroundColor: colors.background }\n : [s.cardFrame, { borderRadius: radius.xl, borderColor: colors.border, backgroundColor: colors.card }],\n style,\n ]}\n {...rest}\n />\n );\n}\n\nconst today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate());\n\ntype Opens = Exclude<DatePickerPresentation, 'auto'>;\n\nconst OPENS: { key: Opens; label: string }[] = [\n { key: 'sheet', label: 'Sheet' },\n { key: 'dialog', label: 'Modal' },\n { key: 'popover', label: 'Popover' },\n];\n\n/** Card header with a compact switch for how the picker opens. */\nfunction Header({ title, opens, onOpens }: { title: string; opens: Opens; onOpens: (o: Opens) => void }) {\n const { colors, radius } = useKit();\n return (\n <View style={s.header}>\n <Text style={[s.title, { color: colors.foreground }]} numberOfLines={1}>\n {title}\n </Text>\n <View style={[s.switch, { backgroundColor: colors.muted, borderRadius: radius.md }]} accessibilityRole=\"radiogroup\">\n {OPENS.map((o) => {\n const on = o.key === opens;\n return (\n <Pressable\n key={o.key}\n onPress={() => onOpens(o.key)}\n accessibilityRole=\"radio\"\n accessibilityState={{ checked: on }}\n accessibilityLabel={`Open as ${o.label}`}\n style={[s.switchItem, { borderRadius: Math.max(0, radius.md - 3) }, on && { backgroundColor: colors.background }]}\n >\n <Text style={[s.switchText, { color: on ? colors.foreground : colors.mutedForeground }]}>{o.label}</Text>\n </Pressable>\n );\n })}\n </View>\n </View>\n );\n}\n\nconst s = StyleSheet.create({\n card: { padding: 16, gap: 14 },\n cardFrame: { borderWidth: StyleSheet.hairlineWidth },\n header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10 },\n title: { flexShrink: 1, fontSize: 15, fontWeight: '400' },\n switch: { flexDirection: 'row', padding: 3 },\n switchItem: { height: 28, paddingHorizontal: 10, alignItems: 'center', justifyContent: 'center' },\n switchText: { fontSize: 12.5, fontWeight: '400' },\n label: { fontSize: 12, fontWeight: '400', letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: -6 },\n button: { height: 46, alignItems: 'center', justifyContent: 'center' },\n buttonText: { fontSize: 15, fontWeight: '400' },\n});\n\nexport default function Birthday({ screen }: DemoProps) {\n const [date, setDate] = useState<Date | null>(new Date(1994, 5, 12));\n const age = date ? Math.floor((today.getTime() - date.getTime()) / (365.25 * 86400000)) : null;\n const [opens, setOpens] = useState<Opens>('sheet');\n return (\n <Card screen={screen}>\n <Header title=\"Profile\" opens={opens} onOpens={setOpens} />\n <DatePicker\n key={opens}\n presentation={opens}\n label=\"Date of birth\"\n value={date}\n onChange={setDate}\n maxDate={today}\n formatOptions={{ day: 'numeric', month: 'long', year: 'numeric' }}\n helper={age !== null ? `You're ${age}. Tap the month title to jump years.` : 'Used to personalise your plan.'}\n />\n </Card>\n );\n}\n"
|
|
252
252
|
},
|
|
253
253
|
{
|
|
254
254
|
"name": "TripDates",
|
|
255
255
|
"title": "Trip Dates",
|
|
256
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type TextProps, type ViewProps } from 'react-native';\nimport { DatePicker, type DatePickerPresentation, type DateRange } from '@/components/ui/date-picker';\nimport { useKit } from '@/lib/kit';\n\n/** Demo card that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Card({ style, ...rest }: ViewProps) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n s.card,\n { borderRadius: radius.xl, borderColor: colors.border, backgroundColor: colors.card },\n style,\n ]}\n {...rest}\n />\n );\n}\n\nconst today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate());\n\ntype Opens = Exclude<DatePickerPresentation, 'auto'>;\n\nconst OPENS: { key: Opens; label: string }[] = [\n { key: 'sheet', label: 'Sheet' },\n { key: 'dialog', label: 'Modal' },\n { key: 'popover', label: 'Popover' },\n];\n\n/** Card header with a compact switch for how the picker opens. */\nfunction Header({ title, opens, onOpens }: { title: string; opens: Opens; onOpens: (o: Opens) => void }) {\n const { colors, radius } = useKit();\n return (\n <View style={s.header}>\n <Text style={[s.title, { color: colors.foreground }]} numberOfLines={1}>\n {title}\n </Text>\n <View style={[s.switch, { backgroundColor: colors.muted, borderRadius: radius.md }]} accessibilityRole=\"radiogroup\">\n {OPENS.map((o) => {\n const on = o.key === opens;\n return (\n <Pressable\n key={o.key}\n onPress={() => onOpens(o.key)}\n accessibilityRole=\"radio\"\n accessibilityState={{ checked: on }}\n accessibilityLabel={`Open as ${o.label}`}\n style={[s.switchItem, { borderRadius: Math.max(0, radius.md - 3) }, on && { backgroundColor: colors.background }]}\n >\n <Text style={[s.switchText, { color: on ? colors.foreground : colors.mutedForeground }]}>{o.label}</Text>\n </Pressable>\n );\n })}\n </View>\n </View>\n );\n}\n\nconst s = StyleSheet.create({\n card: { padding: 16, borderWidth: StyleSheet.hairlineWidth
|
|
256
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type TextProps, type ViewProps } from 'react-native';\nimport { DatePicker, type DatePickerPresentation, type DateRange } from '@/components/ui/date-picker';\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\n/** Demo card that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Card({ screen, style, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n s.card,\n screen\n ? { flex: 1, backgroundColor: colors.background }\n : [s.cardFrame, { borderRadius: radius.xl, borderColor: colors.border, backgroundColor: colors.card }],\n style,\n ]}\n {...rest}\n />\n );\n}\n\nconst today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate());\n\ntype Opens = Exclude<DatePickerPresentation, 'auto'>;\n\nconst OPENS: { key: Opens; label: string }[] = [\n { key: 'sheet', label: 'Sheet' },\n { key: 'dialog', label: 'Modal' },\n { key: 'popover', label: 'Popover' },\n];\n\n/** Card header with a compact switch for how the picker opens. */\nfunction Header({ title, opens, onOpens }: { title: string; opens: Opens; onOpens: (o: Opens) => void }) {\n const { colors, radius } = useKit();\n return (\n <View style={s.header}>\n <Text style={[s.title, { color: colors.foreground }]} numberOfLines={1}>\n {title}\n </Text>\n <View style={[s.switch, { backgroundColor: colors.muted, borderRadius: radius.md }]} accessibilityRole=\"radiogroup\">\n {OPENS.map((o) => {\n const on = o.key === opens;\n return (\n <Pressable\n key={o.key}\n onPress={() => onOpens(o.key)}\n accessibilityRole=\"radio\"\n accessibilityState={{ checked: on }}\n accessibilityLabel={`Open as ${o.label}`}\n style={[s.switchItem, { borderRadius: Math.max(0, radius.md - 3) }, on && { backgroundColor: colors.background }]}\n >\n <Text style={[s.switchText, { color: on ? colors.foreground : colors.mutedForeground }]}>{o.label}</Text>\n </Pressable>\n );\n })}\n </View>\n </View>\n );\n}\n\nconst s = StyleSheet.create({\n card: { padding: 16, gap: 14 },\n cardFrame: { borderWidth: StyleSheet.hairlineWidth },\n header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10 },\n title: { flexShrink: 1, fontSize: 15, fontWeight: '400' },\n switch: { flexDirection: 'row', padding: 3 },\n switchItem: { height: 28, paddingHorizontal: 10, alignItems: 'center', justifyContent: 'center' },\n switchText: { fontSize: 12.5, fontWeight: '400' },\n label: { fontSize: 12, fontWeight: '400', letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: -6 },\n button: { height: 46, alignItems: 'center', justifyContent: 'center' },\n buttonText: { fontSize: 15, fontWeight: '400' },\n});\n\nexport default function TripDates({ screen }: DemoProps) {\n const [trip, setTrip] = useState<DateRange | null>(null);\n const nights = trip?.start && trip.end ? Math.round((trip.end.getTime() - trip.start.getTime()) / 86400000) : 0;\n const [opens, setOpens] = useState<Opens>('dialog');\n return (\n <Card screen={screen}>\n <Header title=\"Trip · Kyoto\" opens={opens} onOpens={setOpens} />\n <DatePicker\n key={opens}\n presentation={opens}\n mode=\"range\"\n label=\"Check-in – Check-out\"\n title=\"When are you travelling?\"\n placeholder=\"Add dates\"\n value={trip}\n onChange={setTrip}\n presets\n confirm\n clearable\n minDate={today}\n success={nights ? `${nights} night${nights > 1 ? 's' : ''} · free cancellation until 48h before` : undefined}\n />\n </Card>\n );\n}\n"
|
|
257
257
|
},
|
|
258
258
|
{
|
|
259
259
|
"name": "Deadline",
|
|
260
260
|
"title": "Deadline",
|
|
261
|
-
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type TextProps, type ViewProps } from 'react-native';\nimport { DatePicker, type DatePickerPresentation, type DateRange } from '@/components/ui/date-picker';\nimport { useKit } from '@/lib/kit';\n\n/** Demo card that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Card({ style, ...rest }: ViewProps) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n s.card,\n { borderRadius: radius.xl, borderColor: colors.border, backgroundColor: colors.card },\n style,\n ]}\n {...rest}\n />\n );\n}\n\nconst today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate());\n\ntype Opens = Exclude<DatePickerPresentation, 'auto'>;\n\nconst OPENS: { key: Opens; label: string }[] = [\n { key: 'sheet', label: 'Sheet' },\n { key: 'dialog', label: 'Modal' },\n { key: 'popover', label: 'Popover' },\n];\n\n/** Card header with a compact switch for how the picker opens. */\nfunction Header({ title, opens, onOpens }: { title: string; opens: Opens; onOpens: (o: Opens) => void }) {\n const { colors, radius } = useKit();\n return (\n <View style={s.header}>\n <Text style={[s.title, { color: colors.foreground }]} numberOfLines={1}>\n {title}\n </Text>\n <View style={[s.switch, { backgroundColor: colors.muted, borderRadius: radius.md }]} accessibilityRole=\"radiogroup\">\n {OPENS.map((o) => {\n const on = o.key === opens;\n return (\n <Pressable\n key={o.key}\n onPress={() => onOpens(o.key)}\n accessibilityRole=\"radio\"\n accessibilityState={{ checked: on }}\n accessibilityLabel={`Open as ${o.label}`}\n style={[s.switchItem, { borderRadius: Math.max(0, radius.md - 3) }, on && { backgroundColor: colors.background }]}\n >\n <Text style={[s.switchText, { color: on ? colors.foreground : colors.mutedForeground }]}>{o.label}</Text>\n </Pressable>\n );\n })}\n </View>\n </View>\n );\n}\n\nconst s = StyleSheet.create({\n card: { padding: 16, borderWidth: StyleSheet.hairlineWidth
|
|
261
|
+
"code": "import React, { useState } from 'react';\nimport { Pressable, StyleSheet, Text, View, type TextProps, type ViewProps } from 'react-native';\nimport { DatePicker, type DatePickerPresentation, type DateRange } from '@/components/ui/date-picker';\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\n/** Demo card that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Card({ screen, style, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n s.card,\n screen\n ? { flex: 1, backgroundColor: colors.background }\n : [s.cardFrame, { borderRadius: radius.xl, borderColor: colors.border, backgroundColor: colors.card }],\n style,\n ]}\n {...rest}\n />\n );\n}\n\nconst today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate());\n\ntype Opens = Exclude<DatePickerPresentation, 'auto'>;\n\nconst OPENS: { key: Opens; label: string }[] = [\n { key: 'sheet', label: 'Sheet' },\n { key: 'dialog', label: 'Modal' },\n { key: 'popover', label: 'Popover' },\n];\n\n/** Card header with a compact switch for how the picker opens. */\nfunction Header({ title, opens, onOpens }: { title: string; opens: Opens; onOpens: (o: Opens) => void }) {\n const { colors, radius } = useKit();\n return (\n <View style={s.header}>\n <Text style={[s.title, { color: colors.foreground }]} numberOfLines={1}>\n {title}\n </Text>\n <View style={[s.switch, { backgroundColor: colors.muted, borderRadius: radius.md }]} accessibilityRole=\"radiogroup\">\n {OPENS.map((o) => {\n const on = o.key === opens;\n return (\n <Pressable\n key={o.key}\n onPress={() => onOpens(o.key)}\n accessibilityRole=\"radio\"\n accessibilityState={{ checked: on }}\n accessibilityLabel={`Open as ${o.label}`}\n style={[s.switchItem, { borderRadius: Math.max(0, radius.md - 3) }, on && { backgroundColor: colors.background }]}\n >\n <Text style={[s.switchText, { color: on ? colors.foreground : colors.mutedForeground }]}>{o.label}</Text>\n </Pressable>\n );\n })}\n </View>\n </View>\n );\n}\n\nconst s = StyleSheet.create({\n card: { padding: 16, gap: 14 },\n cardFrame: { borderWidth: StyleSheet.hairlineWidth },\n header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10 },\n title: { flexShrink: 1, fontSize: 15, fontWeight: '400' },\n switch: { flexDirection: 'row', padding: 3 },\n switchItem: { height: 28, paddingHorizontal: 10, alignItems: 'center', justifyContent: 'center' },\n switchText: { fontSize: 12.5, fontWeight: '400' },\n label: { fontSize: 12, fontWeight: '400', letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: -6 },\n button: { height: 46, alignItems: 'center', justifyContent: 'center' },\n buttonText: { fontSize: 15, fontWeight: '400' },\n});\n\nexport default function Deadline({ screen }: DemoProps) {\n const { colors, radius } = useKit();\n const [due, setDue] = useState<Date | null>(null);\n const [submitted, setSubmitted] = useState(false);\n const [opens, setOpens] = useState<Opens>('popover');\n return (\n <Card screen={screen}>\n <Header title=\"New task\" opens={opens} onOpens={setOpens} />\n <DatePicker\n key={opens}\n presentation={opens}\n label=\"Due date\"\n value={due}\n onChange={setDue}\n minDate={today}\n isDateDisabled={(d) => d.getDay() === 0 || d.getDay() === 6}\n presets={[\n { label: 'Tomorrow', value: () => new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1) },\n { label: 'End of month', value: () => new Date(today.getFullYear(), today.getMonth() + 1, 0) },\n ]}\n error={submitted && !due ? 'Pick a due date so the team can plan around it' : undefined}\n helper=\"Weekdays only.\"\n />\n <Pressable\n onPress={() => setSubmitted(true)}\n style={({ pressed }) => [s.button, { backgroundColor: colors.accent, borderRadius: radius.md, opacity: pressed ? 0.85 : 1 }]}\n accessibilityRole=\"button\"\n >\n <Text style={[s.buttonText, { color: colors.accentForeground }]}>Create task</Text>\n </Pressable>\n </Card>\n );\n}\n"
|
|
262
262
|
},
|
|
263
263
|
{
|
|
264
264
|
"name": "Sizes",
|
|
265
265
|
"title": "Sizes",
|
|
266
|
-
"code": "import { Pressable, StyleSheet, Text, View, type TextProps, type ViewProps } from 'react-native';\nimport { DatePicker, type DatePickerPresentation, type DateRange } from '@/components/ui/date-picker';\nimport { useKit } from '@/lib/kit';\n\n/** Demo card that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Card({ style, ...rest }: ViewProps) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n s.card,\n { borderRadius: radius.xl, borderColor: colors.border, backgroundColor: colors.card },\n style,\n ]}\n {...rest}\n />\n );\n}\n\n/** Text in the kit's ink: `tone=\"muted\"` for secondary copy. */\nfunction Ink({ tone, style, ...rest }: TextProps & { tone?: 'muted' }) {\n const { colors } = useKit();\n return <Text style={[{ color: tone === 'muted' ? colors.mutedForeground : colors.foreground }, style]} {...rest} />;\n}\n\nconst today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate());\n\nconst s = StyleSheet.create({\n card: { padding: 16, borderWidth: StyleSheet.hairlineWidth
|
|
266
|
+
"code": "import { Pressable, StyleSheet, Text, View, type TextProps, type ViewProps } from 'react-native';\nimport { DatePicker, type DatePickerPresentation, type DateRange } from '@/components/ui/date-picker';\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\n/** Demo card that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Card({ screen, style, ...rest }: ViewProps & { screen?: boolean }) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n s.card,\n screen\n ? { flex: 1, backgroundColor: colors.background }\n : [s.cardFrame, { borderRadius: radius.xl, borderColor: colors.border, backgroundColor: colors.card }],\n style,\n ]}\n {...rest}\n />\n );\n}\n\n/** Text in the kit's ink: `tone=\"muted\"` for secondary copy. */\nfunction Ink({ tone, style, ...rest }: TextProps & { tone?: 'muted' }) {\n const { colors } = useKit();\n return <Text style={[{ color: tone === 'muted' ? colors.mutedForeground : colors.foreground }, style]} {...rest} />;\n}\n\nconst today = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate());\n\nconst s = StyleSheet.create({\n card: { padding: 16, gap: 14 },\n cardFrame: { borderWidth: StyleSheet.hairlineWidth },\n header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10 },\n title: { flexShrink: 1, fontSize: 15, fontWeight: '400' },\n switch: { flexDirection: 'row', padding: 3 },\n switchItem: { height: 28, paddingHorizontal: 10, alignItems: 'center', justifyContent: 'center' },\n switchText: { fontSize: 12.5, fontWeight: '400' },\n label: { fontSize: 12, fontWeight: '400', letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: -6 },\n button: { height: 46, alignItems: 'center', justifyContent: 'center' },\n buttonText: { fontSize: 15, fontWeight: '400' },\n});\n\nexport default function Sizes({ screen }: DemoProps) {\n const d = new Date(today.getFullYear(), today.getMonth(), 24);\n return (\n <Card screen={screen}>\n <Ink tone=\"muted\" style={s.label}>Small</Ink>\n <DatePicker size=\"sm\" label=\"Invoice date\" defaultValue={d} />\n <Ink tone=\"muted\" style={s.label}>Medium</Ink>\n <DatePicker size=\"md\" label=\"Invoice date\" defaultValue={d} />\n <Ink tone=\"muted\" style={s.label}>Large</Ink>\n <DatePicker size=\"lg\" label=\"Invoice date\" defaultValue={d} />\n <Ink tone=\"muted\" style={s.label}>Disabled</Ink>\n <DatePicker disabled label=\"Invoice date\" defaultValue={d} />\n </Card>\n );\n}\n"
|
|
267
267
|
}
|
|
268
268
|
]
|
|
269
269
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "donut-shop",
|
|
3
|
+
"category": "Blocks",
|
|
4
|
+
"aliases": [],
|
|
5
|
+
"exportName": "DonutShop",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/components/blocks/donut-shop.tsx"
|
|
8
|
+
],
|
|
9
|
+
"summary": "Donuts roll across a carousel and zoom into a detail view beside their nutrition.",
|
|
10
|
+
"props": [
|
|
11
|
+
[
|
|
12
|
+
"items",
|
|
13
|
+
"DonutItem[]",
|
|
14
|
+
"5 donuts",
|
|
15
|
+
"{ name, price, icing, topping?, toppingColor?, image?, nutrition? }."
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
"title",
|
|
19
|
+
"string",
|
|
20
|
+
"'Dunats'",
|
|
21
|
+
"Shop name, in a script face."
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
"currency",
|
|
25
|
+
"string",
|
|
26
|
+
"'$'",
|
|
27
|
+
"Prefix for prices."
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
"background",
|
|
31
|
+
"string",
|
|
32
|
+
"soft pink",
|
|
33
|
+
"Page colour in light mode."
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"initialIndex",
|
|
37
|
+
"number",
|
|
38
|
+
"0",
|
|
39
|
+
"Donut shown first."
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
"onIndexChange",
|
|
43
|
+
"(index: number) => void",
|
|
44
|
+
"—",
|
|
45
|
+
"The centred donut changed."
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
"onAddToCart",
|
|
49
|
+
"(item, quantity) => void",
|
|
50
|
+
"—",
|
|
51
|
+
"Add to Cart pressed."
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"style",
|
|
55
|
+
"StyleProp<ViewStyle>",
|
|
56
|
+
"flex: 1",
|
|
57
|
+
"Root style."
|
|
58
|
+
]
|
|
59
|
+
],
|
|
60
|
+
"behavior": [
|
|
61
|
+
[
|
|
62
|
+
"Swipe",
|
|
63
|
+
"Donuts roll a full turn per circumference travelled and snap to the nearest; the centre one is larger."
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"+",
|
|
67
|
+
"Grows into a + 01 − stepper; an add-to-cart bar with the running total rises from the bottom."
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"Detail",
|
|
71
|
+
"Tap the donut: the list pushes away, the donut zooms to the right edge and keeps turning, the name and nutrition chips slide in. Back reverses it."
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
"Art",
|
|
75
|
+
"Drawn donuts per item (icing colour, sprinkles / drizzle / specks), or your own image per item."
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"Haptics",
|
|
79
|
+
"A tick on each snap and a medium impact on Add to Cart."
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
"Reduced motion",
|
|
83
|
+
"The detail donut stops its idle spin."
|
|
84
|
+
]
|
|
85
|
+
],
|
|
86
|
+
"usage": null,
|
|
87
|
+
"examples": []
|
|
88
|
+
}
|
|
@@ -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/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, ...rest }: ViewProps) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n {\n height: 600,\n borderRadius: radius.scale(28),\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: colors.border,\n backgroundColor: colors.background,\n overflow: 'hidden',\n },\n style,\n ]}\n {...rest}\n />\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 return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n <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() {\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>\n <Drawer\n variant=\"push\"\n width=\"78%\"\n topInset={STATUS_H}\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,\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\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"
|
|
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/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, ...rest }: ViewProps) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n {\n height: 600,\n borderRadius: radius.scale(28),\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: colors.border,\n backgroundColor: colors.background,\n overflow: 'hidden',\n },\n style,\n ]}\n {...rest}\n />\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 return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n <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() {\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>\n <Drawer\n topInset={STATUS_H}\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\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"
|
|
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/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, ...rest }: ViewProps) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n {\n height: 600,\n borderRadius: radius.scale(28),\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: colors.border,\n backgroundColor: colors.background,\n overflow: 'hidden',\n },\n style,\n ]}\n {...rest}\n />\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 return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n <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() {\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>\n <Drawer\n open={open}\n onOpenChange={setOpen}\n side=\"right\"\n width=\"86%\"\n topInset={STATUS_H}\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\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"
|
|
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/** Phone-sized frame that follows the kit theme, so the docs radius / accent picker reshapes it too. */\nfunction Frame({ style, ...rest }: ViewProps) {\n const { colors, radius } = useKit();\n return (\n <View\n style={[\n {\n height: 600,\n borderRadius: radius.scale(28),\n borderWidth: StyleSheet.hairlineWidth,\n borderColor: colors.border,\n backgroundColor: colors.background,\n overflow: 'hidden',\n },\n style,\n ]}\n {...rest}\n />\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 return (\n <View style={{ flex: 1, backgroundColor: colors.background }}>\n <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() {\n const { colors, radius, dark } = useKit();\n const [view, setView] = useState('Listen now');\n const [playing, setPlaying] = useState(true);\n return (\n <Frame>\n <Drawer\n variant=\"push\"\n width={252}\n topInset={STATUS_H}\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\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"
|
|
197
197
|
}
|
|
198
198
|
]
|
|
199
199
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fintech-app",
|
|
3
|
+
"category": "Templates",
|
|
4
|
+
"aliases": [],
|
|
5
|
+
"exportName": null,
|
|
6
|
+
"files": [
|
|
7
|
+
"src/app/(tabs)/_layout.tsx",
|
|
8
|
+
"src/app/(tabs)/cards.tsx",
|
|
9
|
+
"src/app/(tabs)/index.tsx",
|
|
10
|
+
"src/app/(tabs)/pay.tsx",
|
|
11
|
+
"src/app/(tabs)/profile.tsx",
|
|
12
|
+
"src/app/_layout.tsx",
|
|
13
|
+
"src/app/onboarding.tsx",
|
|
14
|
+
"src/app/settings.tsx",
|
|
15
|
+
"src/app/sign-in.tsx",
|
|
16
|
+
"src/components/fintech-app/cards-screen.tsx",
|
|
17
|
+
"src/components/fintech-app/pay-screen.tsx",
|
|
18
|
+
"src/components/fintech-app/tabs.tsx",
|
|
19
|
+
"src/lib/fintech-app/data.tsx",
|
|
20
|
+
"src/lib/fintech-app/session.tsx",
|
|
21
|
+
"src/lib/fintech-app/wallet.tsx"
|
|
22
|
+
],
|
|
23
|
+
"summary": null,
|
|
24
|
+
"props": [],
|
|
25
|
+
"behavior": [
|
|
26
|
+
[
|
|
27
|
+
"Flow",
|
|
28
|
+
"Onboarding shows once, then phone number + one-time code, then the tabs. Signing out returns to sign in. The session is saved on the device (AsyncStorage) and guarded with Expo Router’s Stack.Protected."
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
"Tabs",
|
|
32
|
+
"Home (scrubbable revenue chart, KPIs, pull to refresh), Cards (swipe between cards, reveal the number or CVV, freeze), Pay (payee, amount, slide to send) and Profile."
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
"Payments",
|
|
36
|
+
"A transfer made on Pay appears at the top of Home’s transactions straight away. The send itself is a stand-in — call your payments API from the slider’s onConfirm."
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
"Settings",
|
|
40
|
+
"Pushed from Profile. Appearance (System / Light / Dark) re-themes every kit component instantly."
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
"Your backend",
|
|
44
|
+
"SMS code handlers in app/sign-in.tsx and the transfer in components/fintech-app/pay-screen.tsx are stand-ins that wait and succeed. Cards, payees and the dashboard numbers live in lib/fintech-app/data.tsx."
|
|
45
|
+
]
|
|
46
|
+
],
|
|
47
|
+
"usage": null,
|
|
48
|
+
"examples": []
|
|
49
|
+
}
|