@krapwoo/ds-viewer 0.1.1 → 0.2.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 +100 -78
- package/dist/cli/config.js +26 -1
- package/dist/cli/dev.js +50 -5
- package/dist/cli/glob.js +8 -0
- package/dist/cli/init.js +148 -18
- package/dist/cli/main.js +66 -4
- package/dist/cli/props.js +166 -18
- package/dist/cli/sync.js +17 -1
- package/dist/cli/workspace.js +49 -21
- package/native/catalog/CatalogShell.tsx +6 -1
- package/native/catalog/CatalogSidebar.tsx +98 -5
- package/native/catalog/TokenRow.tsx +3 -2
- package/native/catalog/index.ts +6 -5
- package/native/catalog/logoLayout.ts +12 -0
- package/native/catalog/pageApi.ts +10 -1
- package/native/catalog/tokens.ts +5 -0
- package/package.json +8 -6
- package/starter-kit/WHEN_TO_USE.md +124 -0
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.catalog.tsx +23 -0
- package/starter-kit/components/AnimatedChevron/AnimatedChevron.tsx +46 -0
- package/starter-kit/components/AnimatedChevron/index.ts +2 -0
- package/starter-kit/components/Avatar/Avatar.catalog.tsx +72 -0
- package/starter-kit/components/Avatar/Avatar.tsx +74 -0
- package/starter-kit/components/Avatar/index.ts +2 -0
- package/starter-kit/components/Badge/Badge.catalog.tsx +96 -0
- package/starter-kit/components/Badge/Badge.tsx +56 -0
- package/starter-kit/components/Badge/Badge.types.ts +18 -0
- package/starter-kit/components/Badge/index.ts +2 -0
- package/starter-kit/components/Banner/Banner.catalog.tsx +87 -0
- package/starter-kit/components/Banner/Banner.tsx +257 -0
- package/starter-kit/components/Banner/Banner.types.ts +38 -0
- package/starter-kit/components/Banner/index.ts +2 -0
- package/starter-kit/components/BottomSheet/BottomSheet.catalog.tsx +77 -0
- package/starter-kit/components/BottomSheet/BottomSheet.tsx +151 -0
- package/starter-kit/components/BottomSheet/index.ts +2 -0
- package/starter-kit/components/Button/Button.catalog.tsx +113 -0
- package/starter-kit/components/Button/Button.tsx +173 -0
- package/starter-kit/components/Button/Button.types.ts +52 -0
- package/starter-kit/components/Button/index.ts +2 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.catalog.tsx +39 -0
- package/starter-kit/components/ButtonGroup/ButtonGroup.tsx +84 -0
- package/starter-kit/components/ButtonGroup/index.ts +2 -0
- package/starter-kit/components/Card/Card.catalog.tsx +68 -0
- package/starter-kit/components/Card/Card.tsx +133 -0
- package/starter-kit/components/Card/index.ts +2 -0
- package/starter-kit/components/Checkbox/Checkbox.catalog.tsx +28 -0
- package/starter-kit/components/Checkbox/Checkbox.tsx +104 -0
- package/starter-kit/components/Checkbox/index.ts +2 -0
- package/starter-kit/components/Dialog/Dialog.catalog.tsx +41 -0
- package/starter-kit/components/Dialog/Dialog.tsx +82 -0
- package/starter-kit/components/Dialog/index.ts +2 -0
- package/starter-kit/components/Divider/Divider.catalog.tsx +35 -0
- package/starter-kit/components/Divider/Divider.tsx +20 -0
- package/starter-kit/components/Divider/index.ts +2 -0
- package/starter-kit/components/Dock/Dock.catalog.tsx +71 -0
- package/starter-kit/components/Dock/Dock.tsx +58 -0
- package/starter-kit/components/Dock/index.ts +2 -0
- package/starter-kit/components/Dropdown/Dropdown.catalog.tsx +69 -0
- package/starter-kit/components/Dropdown/Dropdown.tsx +162 -0
- package/starter-kit/components/Dropdown/index.ts +2 -0
- package/starter-kit/components/EmptyState/EmptyState.catalog.tsx +67 -0
- package/starter-kit/components/EmptyState/EmptyState.tsx +88 -0
- package/starter-kit/components/EmptyState/index.ts +2 -0
- package/starter-kit/components/FieldContainer/FieldContainer.catalog.tsx +76 -0
- package/starter-kit/components/FieldContainer/FieldContainer.tsx +99 -0
- package/starter-kit/components/FieldContainer/FieldContainer.types.ts +27 -0
- package/starter-kit/components/FieldContainer/index.ts +2 -0
- package/starter-kit/components/InputClearButton/InputClearButton.catalog.tsx +18 -0
- package/starter-kit/components/InputClearButton/InputClearButton.tsx +63 -0
- package/starter-kit/components/InputClearButton/index.ts +2 -0
- package/starter-kit/components/InputField/InputField.catalog.tsx +76 -0
- package/starter-kit/components/InputField/InputField.tsx +418 -0
- package/starter-kit/components/InputField/index.ts +2 -0
- package/starter-kit/components/List/List.catalog.tsx +30 -0
- package/starter-kit/components/List/List.tsx +36 -0
- package/starter-kit/components/List/index.ts +2 -0
- package/starter-kit/components/ListItem/ListItem.catalog.tsx +89 -0
- package/starter-kit/components/ListItem/ListItem.tsx +165 -0
- package/starter-kit/components/ListItem/index.ts +2 -0
- package/starter-kit/components/Loading/Loading.catalog.tsx +34 -0
- package/starter-kit/components/Loading/Loading.tsx +121 -0
- package/starter-kit/components/Loading/index.ts +2 -0
- package/starter-kit/components/Pill/Pill.catalog.tsx +93 -0
- package/starter-kit/components/Pill/Pill.tsx +181 -0
- package/starter-kit/components/Pill/Pill.types.ts +30 -0
- package/starter-kit/components/Pill/index.ts +2 -0
- package/starter-kit/components/PillRow/PillRow.catalog.tsx +91 -0
- package/starter-kit/components/PillRow/PillRow.tsx +140 -0
- package/starter-kit/components/PillRow/PillRow.types.ts +24 -0
- package/starter-kit/components/PillRow/index.ts +2 -0
- package/starter-kit/components/ProgressDots/ProgressDots.catalog.tsx +22 -0
- package/starter-kit/components/ProgressDots/ProgressDots.tsx +42 -0
- package/starter-kit/components/ProgressDots/index.ts +2 -0
- package/starter-kit/components/Radio/Radio.catalog.tsx +38 -0
- package/starter-kit/components/Radio/Radio.tsx +114 -0
- package/starter-kit/components/Radio/index.ts +2 -0
- package/starter-kit/components/Radio/usePressScale.ts +33 -0
- package/starter-kit/components/SearchField/SearchField.catalog.tsx +28 -0
- package/starter-kit/components/SearchField/SearchField.tsx +98 -0
- package/starter-kit/components/SearchField/index.ts +2 -0
- package/starter-kit/components/SectionHeader/SectionHeader.catalog.tsx +72 -0
- package/starter-kit/components/SectionHeader/SectionHeader.tsx +139 -0
- package/starter-kit/components/SectionHeader/index.ts +2 -0
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.catalog.tsx +31 -0
- package/starter-kit/components/SegmentedToggle/SegmentedToggle.tsx +163 -0
- package/starter-kit/components/SegmentedToggle/index.ts +2 -0
- package/starter-kit/components/SegmentedToggle/useSlideAnim.ts +29 -0
- package/starter-kit/components/Shimmer/Breathing.catalog.tsx +29 -0
- package/starter-kit/components/Shimmer/Shimmer.catalog.tsx +89 -0
- package/starter-kit/components/Shimmer/Shimmer.tsx +158 -0
- package/starter-kit/components/Shimmer/Shimmer.types.ts +27 -0
- package/starter-kit/components/Shimmer/SkeletonGroup.catalog.tsx +39 -0
- package/starter-kit/components/Shimmer/index.ts +2 -0
- package/starter-kit/components/Surface/Surface.catalog.tsx +44 -0
- package/starter-kit/components/Surface/Surface.tsx +45 -0
- package/starter-kit/components/Surface/index.ts +2 -0
- package/starter-kit/components/Switch/Switch.catalog.tsx +33 -0
- package/starter-kit/components/Switch/Switch.tsx +129 -0
- package/starter-kit/components/Switch/index.ts +2 -0
- package/starter-kit/components/TextArea/TextArea.catalog.tsx +29 -0
- package/starter-kit/components/TextArea/TextArea.tsx +99 -0
- package/starter-kit/components/TextArea/index.ts +2 -0
- package/starter-kit/components/Toast/Toast.catalog.tsx +61 -0
- package/starter-kit/components/Toast/Toast.tsx +144 -0
- package/starter-kit/components/Toast/index.ts +2 -0
- package/starter-kit/components/Tooltip/Tooltip.catalog.tsx +121 -0
- package/starter-kit/components/Tooltip/Tooltip.tsx +195 -0
- package/starter-kit/components/Tooltip/index.ts +2 -0
- package/starter-kit/components/TopNav/TopNav.catalog.tsx +71 -0
- package/starter-kit/components/TopNav/TopNav.tsx +77 -0
- package/starter-kit/components/TopNav/index.ts +2 -0
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.catalog.tsx +30 -0
- package/starter-kit/components/UnderlineTabs/UnderlineTabs.tsx +156 -0
- package/starter-kit/components/UnderlineTabs/index.ts +2 -0
- package/starter-kit/components/index.ts +36 -0
- package/starter-kit/components/statusColors.ts +27 -0
- package/starter-kit/icons/Icon.native.tsx +90 -0
- package/starter-kit/icons/index.ts +10 -0
- package/starter-kit/icons/paths.ts +623 -0
- package/starter-kit/icons/types.ts +69 -0
- package/starter-kit/pages/Colors.catalog.tsx +55 -0
- package/starter-kit/pages/Font.catalog.tsx +45 -0
- package/starter-kit/pages/Icons.catalog.tsx +51 -0
- package/starter-kit/pages/Manifest.catalog.tsx +24 -0
- package/starter-kit/pages/Motion.catalog.tsx +101 -0
- package/starter-kit/pages/Radius.catalog.tsx +35 -0
- package/starter-kit/pages/ReportIssue.catalog.tsx +148 -0
- package/starter-kit/pages/SavedTrips.catalog.tsx +247 -0
- package/starter-kit/pages/Shadow.catalog.tsx +34 -0
- package/starter-kit/pages/Spacing.catalog.tsx +12 -0
- package/starter-kit/pages/TripPlannerForm.catalog.tsx +48 -0
- package/starter-kit/pages/Typography.catalog.tsx +17 -0
- package/starter-kit/tokens/index.ts +50 -0
- package/starter-kit/tokens/motion.ts +90 -0
- package/starter-kit/tokens/palette.ts +111 -0
- package/starter-kit/tokens/scales.ts +106 -0
- package/starter-kit/tokens/semantic.ts +98 -0
- package/starter-kit/tokens/shadow.ts +36 -0
- package/starter-kit/tokens/typography.ts +72 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { IconName } from '../../icons';
|
|
3
|
+
|
|
4
|
+
export type BadgeVariant = 'neutral' | 'info' | 'positive' | 'warning' | 'negative';
|
|
5
|
+
|
|
6
|
+
export interface BadgeProps {
|
|
7
|
+
/** Semantic color scheme. */
|
|
8
|
+
variant?: BadgeVariant;
|
|
9
|
+
/** Label text. Omit (with a single icon) for an icon-only chip. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Icon before the label. */
|
|
12
|
+
leadingIcon?: IconName;
|
|
13
|
+
/** Icon after the label. */
|
|
14
|
+
trailingIcon?: IconName;
|
|
15
|
+
/** Accessible name — required for an icon-only badge (no `label`) so it can be announced. */
|
|
16
|
+
accessibilityLabel?: string;
|
|
17
|
+
style?: StyleProp<ViewStyle>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Banner } from './Banner';
|
|
4
|
+
|
|
5
|
+
export default defineCatalogPage({
|
|
6
|
+
component: 'Banner',
|
|
7
|
+
group: 'Surfaces',
|
|
8
|
+
description: 'An inline callout for status/announcements — five semantic variants, optional collapsible body, inline link, and action button.',
|
|
9
|
+
whenToUse: "Persistent and in-flow, describing a standing condition about the screen's content. For a transient, self-contained event notification, use Toast instead.",
|
|
10
|
+
a11y: 'When onPress/action is set the header/button are Pressables; the collapsible header toggles the description with a chevron affordance.',
|
|
11
|
+
variants: {
|
|
12
|
+
itemsFill: true,
|
|
13
|
+
items: [
|
|
14
|
+
{ key: 'neutral', name: 'Neutral', props: { variant: 'neutral' }, node: <Banner variant="neutral" title="Schedule notice" description="Holiday schedule in effect Monday." /> },
|
|
15
|
+
{ key: 'info', name: 'Info', props: { variant: 'info' }, node: <Banner variant="info" title="Weekend service change" description="The 6 runs express in both directions this weekend." /> },
|
|
16
|
+
{ key: 'positive', name: 'Positive', props: { variant: 'positive' }, node: <Banner variant="positive" title="Service restored" description="All lines are running on a normal schedule." /> },
|
|
17
|
+
{ key: 'warning', name: 'Warning', props: { variant: 'warning' }, node: <Banner variant="warning" title="Delays" description="Signal problems near 14 St." /> },
|
|
18
|
+
{ key: 'negative', name: 'Negative', props: { variant: 'negative' }, node: <Banner variant="negative" title="Line suspended" description="No service between 96 St and 137 St until further notice." /> },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
states: {
|
|
22
|
+
itemsFill: true,
|
|
23
|
+
items: [
|
|
24
|
+
{
|
|
25
|
+
key: 'tappable',
|
|
26
|
+
name: 'Tappable',
|
|
27
|
+
node: (
|
|
28
|
+
<Banner
|
|
29
|
+
variant="neutral"
|
|
30
|
+
title="Alert preferences"
|
|
31
|
+
description="Tap to manage which alerts you receive."
|
|
32
|
+
onPress={() => {}}
|
|
33
|
+
/>
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'collapsible-expanded',
|
|
38
|
+
name: 'Collapsible (expanded)',
|
|
39
|
+
node: (
|
|
40
|
+
<Banner
|
|
41
|
+
variant="neutral"
|
|
42
|
+
title="Trip details"
|
|
43
|
+
description="Board at the front car for a faster transfer at Union Sq."
|
|
44
|
+
collapsible
|
|
45
|
+
/>
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'collapsible-collapsed',
|
|
50
|
+
name: 'Collapsible (collapsed)',
|
|
51
|
+
node: (
|
|
52
|
+
<Banner
|
|
53
|
+
variant="neutral"
|
|
54
|
+
title="Trip details"
|
|
55
|
+
description="Board at the front car for a faster transfer at Union Sq."
|
|
56
|
+
collapsible
|
|
57
|
+
defaultExpanded={false}
|
|
58
|
+
/>
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: 'action',
|
|
63
|
+
name: 'With action',
|
|
64
|
+
node: (
|
|
65
|
+
<Banner
|
|
66
|
+
variant="info"
|
|
67
|
+
title="App update available"
|
|
68
|
+
description="Version 4.2 adds live bus tracking."
|
|
69
|
+
action={{ label: 'Update', onPress: () => {} }}
|
|
70
|
+
/>
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: 'link',
|
|
75
|
+
name: 'With link',
|
|
76
|
+
node: (
|
|
77
|
+
<Banner
|
|
78
|
+
variant="neutral"
|
|
79
|
+
title="Fare increase"
|
|
80
|
+
description="New fares start March 1."
|
|
81
|
+
link={{ label: 'Learn more', onPress: () => {} }}
|
|
82
|
+
/>
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { View, Text, Pressable, Animated, Easing, StyleSheet, type LayoutChangeEvent } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_RADIUS, DS_SPACING, DS_TYPOGRAPHY, DS_ICON_SIZE, DS_MOTION_DURATION, DS_MOTION_EASING } from '../../tokens';
|
|
4
|
+
import { Icon } from '../../icons/Icon.native';
|
|
5
|
+
import type { IconName } from '../../icons';
|
|
6
|
+
import { AnimatedChevron } from '../AnimatedChevron';
|
|
7
|
+
import { STATUS_BG, STATUS_FG } from '../statusColors';
|
|
8
|
+
import type { BannerProps, BannerVariant } from './Banner.types';
|
|
9
|
+
|
|
10
|
+
// Shared with Badge (see ../statusColors) so a banner reads as the same status identity as a
|
|
11
|
+
// badge of the same variant — one map, matched by construction.
|
|
12
|
+
|
|
13
|
+
const BANNER_ICONS: Record<BannerVariant, IconName> = {
|
|
14
|
+
neutral: 'info-circle',
|
|
15
|
+
info: 'info-circle',
|
|
16
|
+
positive: 'circle-check',
|
|
17
|
+
warning: 'triangle-alert',
|
|
18
|
+
negative: 'alert-circle',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const COLLAPSE_ANIM_MS = DS_MOTION_DURATION.base;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A status callout in two layouts, chosen by props:
|
|
25
|
+
* • collapsible callout — the whole card toggles the description open/closed;
|
|
26
|
+
* • standard callout — leading icon + title, optional description/link/action.
|
|
27
|
+
*/
|
|
28
|
+
export function Banner({
|
|
29
|
+
variant = 'warning',
|
|
30
|
+
title = 'Banner title',
|
|
31
|
+
description = 'Description text goes here',
|
|
32
|
+
collapsible = false,
|
|
33
|
+
defaultExpanded = true,
|
|
34
|
+
backgroundColor,
|
|
35
|
+
onPress,
|
|
36
|
+
trailingIcon,
|
|
37
|
+
textColor,
|
|
38
|
+
icon,
|
|
39
|
+
link,
|
|
40
|
+
action,
|
|
41
|
+
style,
|
|
42
|
+
}: BannerProps) {
|
|
43
|
+
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
44
|
+
// Real layout height + opacity via the core `Animated` API (`useNativeDriver: false`) — not
|
|
45
|
+
// `LayoutAnimation`, which is unreliable on the New Architecture (Fabric) and a total no-op on
|
|
46
|
+
// web, so the collapse used to just snap instantly there. Same measure-once-then-animate
|
|
47
|
+
// technique used elsewhere in this DS: render in normal flow only on the very first pass while
|
|
48
|
+
// already expanded (shows instantly, no flash, and captures the real height); every other pass —
|
|
49
|
+
// including the very first while starting collapsed — positions the content absolutely so it can
|
|
50
|
+
// self-measure without affecting layout, then animates between 0 and that measured height.
|
|
51
|
+
const [measuredHeight, setMeasuredHeight] = useState(0);
|
|
52
|
+
const collapseAnim = useRef(new Animated.Value(defaultExpanded ? 1 : 0)).current;
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const anim = Animated.timing(collapseAnim, {
|
|
56
|
+
toValue: expanded ? 1 : 0,
|
|
57
|
+
duration: COLLAPSE_ANIM_MS,
|
|
58
|
+
// `standard` — an in-place expand/collapse (a toggle), per DS_MOTION_EASING_USE.
|
|
59
|
+
easing: Easing.bezier(...DS_MOTION_EASING.standard),
|
|
60
|
+
useNativeDriver: false,
|
|
61
|
+
});
|
|
62
|
+
anim.start();
|
|
63
|
+
return () => anim.stop();
|
|
64
|
+
}, [expanded, collapseAnim]);
|
|
65
|
+
|
|
66
|
+
const bg = backgroundColor ?? STATUS_BG[variant];
|
|
67
|
+
const contentTextColor = textColor ?? STATUS_FG[variant];
|
|
68
|
+
const iconName = icon ?? BANNER_ICONS[variant];
|
|
69
|
+
|
|
70
|
+
// ── Collapsible callout: the whole banner toggles the description ─────────────
|
|
71
|
+
if (collapsible) {
|
|
72
|
+
const toggle = () => setExpanded(e => !e);
|
|
73
|
+
const flowMeasure = measuredHeight === 0 && expanded;
|
|
74
|
+
const onDescriptionLayout = (e: LayoutChangeEvent) => {
|
|
75
|
+
const h = e.nativeEvent.layout.height;
|
|
76
|
+
if (h > 0 && h !== measuredHeight) setMeasuredHeight(h);
|
|
77
|
+
};
|
|
78
|
+
return (
|
|
79
|
+
<Pressable
|
|
80
|
+
style={[styles.calloutContainer, { backgroundColor: bg }, style]}
|
|
81
|
+
onPress={toggle}
|
|
82
|
+
accessibilityRole="button"
|
|
83
|
+
accessibilityState={{ expanded }}
|
|
84
|
+
>
|
|
85
|
+
{({ pressed }) => (
|
|
86
|
+
<>
|
|
87
|
+
{pressed && <View style={styles.pressOverlay} pointerEvents="none" />}
|
|
88
|
+
<View style={styles.headerRow}>
|
|
89
|
+
<Icon name={iconName} size={DS_ICON_SIZE.xs} color={contentTextColor} />
|
|
90
|
+
<Text style={[styles.title, styles.titleFlex, { color: contentTextColor, marginBottom: 0 }]}>
|
|
91
|
+
{title}
|
|
92
|
+
</Text>
|
|
93
|
+
<AnimatedChevron expanded={expanded} size={DS_ICON_SIZE.xs} color={contentTextColor} />
|
|
94
|
+
</View>
|
|
95
|
+
{!!description && (
|
|
96
|
+
<Animated.View
|
|
97
|
+
style={[
|
|
98
|
+
styles.collapseClip,
|
|
99
|
+
measuredHeight === 0
|
|
100
|
+
? expanded
|
|
101
|
+
? null
|
|
102
|
+
: styles.collapseHidden
|
|
103
|
+
: { height: Animated.multiply(collapseAnim, measuredHeight), opacity: collapseAnim },
|
|
104
|
+
]}
|
|
105
|
+
>
|
|
106
|
+
<View style={flowMeasure ? undefined : styles.collapseAbsolute} onLayout={onDescriptionLayout}>
|
|
107
|
+
<Text style={[styles.description, styles.descriptionPad, { color: contentTextColor }]}>
|
|
108
|
+
{description}
|
|
109
|
+
</Text>
|
|
110
|
+
</View>
|
|
111
|
+
</Animated.View>
|
|
112
|
+
)}
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
</Pressable>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Standard callout (title + description) ────────────────────────────────────
|
|
120
|
+
const body = (pressed: boolean) => (
|
|
121
|
+
<>
|
|
122
|
+
{onPress && pressed && <View style={styles.pressOverlay} pointerEvents="none" />}
|
|
123
|
+
<View style={styles.headerRow}>
|
|
124
|
+
<Icon name={iconName} size={DS_ICON_SIZE.xs} color={contentTextColor} />
|
|
125
|
+
<Text style={[styles.title, styles.titleFlex, { color: contentTextColor, marginBottom: 0 }]}>
|
|
126
|
+
{title}
|
|
127
|
+
</Text>
|
|
128
|
+
{trailingIcon ? <Icon name={trailingIcon} size={DS_ICON_SIZE.xs} color={contentTextColor} /> : null}
|
|
129
|
+
</View>
|
|
130
|
+
{description || link ? (
|
|
131
|
+
<Text style={[styles.description, styles.descriptionPad, { color: contentTextColor }]}>
|
|
132
|
+
{description ?? ''}
|
|
133
|
+
{description && link ? ' ' : null}
|
|
134
|
+
{link ? (
|
|
135
|
+
<Text style={styles.linkText} onPress={link.onPress} accessibilityRole="link">
|
|
136
|
+
{link.label}
|
|
137
|
+
</Text>
|
|
138
|
+
) : null}
|
|
139
|
+
</Text>
|
|
140
|
+
) : null}
|
|
141
|
+
{action ? (
|
|
142
|
+
<View style={[styles.actionRow, { paddingHorizontal: DS_SPACING[800] }]}>
|
|
143
|
+
<Pressable
|
|
144
|
+
style={({ pressed }) => [styles.actionButton, pressed && styles.actionButtonPressed]}
|
|
145
|
+
// Pads the compact text button's visual ~26px height out to the 44pt touch-target minimum.
|
|
146
|
+
hitSlop={10}
|
|
147
|
+
onPress={action.onPress}
|
|
148
|
+
accessibilityRole="button"
|
|
149
|
+
accessibilityLabel={action.label}
|
|
150
|
+
>
|
|
151
|
+
<Text style={[styles.actionLabel, { color: contentTextColor }]}>{action.label}</Text>
|
|
152
|
+
</Pressable>
|
|
153
|
+
</View>
|
|
154
|
+
) : null}
|
|
155
|
+
</>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (onPress) {
|
|
159
|
+
return (
|
|
160
|
+
<Pressable
|
|
161
|
+
style={[styles.calloutContainer, { backgroundColor: bg }, style]}
|
|
162
|
+
onPress={onPress}
|
|
163
|
+
accessibilityRole="button"
|
|
164
|
+
accessibilityLabel={title}
|
|
165
|
+
>
|
|
166
|
+
{({ pressed }) => body(pressed)}
|
|
167
|
+
</Pressable>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<View style={[styles.calloutContainer, { backgroundColor: bg }, style]}>{body(false)}</View>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const styles = StyleSheet.create({
|
|
177
|
+
actionRow: {
|
|
178
|
+
flexDirection: 'row',
|
|
179
|
+
justifyContent: 'flex-end',
|
|
180
|
+
marginTop: DS_SPACING[400],
|
|
181
|
+
},
|
|
182
|
+
actionButton: {
|
|
183
|
+
borderRadius: DS_RADIUS.xs,
|
|
184
|
+
paddingVertical: DS_SPACING[200],
|
|
185
|
+
paddingHorizontal: DS_SPACING[600],
|
|
186
|
+
},
|
|
187
|
+
actionButtonPressed: {
|
|
188
|
+
backgroundColor: DS_SEMANTIC.interaction.pressed,
|
|
189
|
+
},
|
|
190
|
+
actionLabel: {
|
|
191
|
+
...DS_TYPOGRAPHY.labelSm,
|
|
192
|
+
},
|
|
193
|
+
// Was undistinguished from the surrounding description text (just inherited its color via
|
|
194
|
+
// nested-Text color inheritance) — underline alone with no color difference is easy to miss at a
|
|
195
|
+
// glance and gives nothing to fall back on if the underline is ever dropped.
|
|
196
|
+
linkText: {
|
|
197
|
+
textDecorationLine: 'underline',
|
|
198
|
+
color: DS_SEMANTIC.text.link,
|
|
199
|
+
},
|
|
200
|
+
// Callout: the header carries the top + horizontal padding so the pressed state fills it; the
|
|
201
|
+
// bottom padding lives on the container (constant) so nothing jumps when toggling — only the
|
|
202
|
+
// description mounts/unmounts. overflow:hidden keeps the rounded corners.
|
|
203
|
+
calloutContainer: {
|
|
204
|
+
borderRadius: DS_RADIUS.medium,
|
|
205
|
+
width: '100%',
|
|
206
|
+
overflow: 'hidden',
|
|
207
|
+
paddingBottom: DS_SPACING[800],
|
|
208
|
+
},
|
|
209
|
+
headerRow: {
|
|
210
|
+
flexDirection: 'row',
|
|
211
|
+
alignItems: 'center',
|
|
212
|
+
justifyContent: 'space-between',
|
|
213
|
+
gap: DS_SPACING[400],
|
|
214
|
+
paddingTop: DS_SPACING[800],
|
|
215
|
+
paddingHorizontal: DS_SPACING[800],
|
|
216
|
+
},
|
|
217
|
+
title: {
|
|
218
|
+
...DS_TYPOGRAPHY.labelSm,
|
|
219
|
+
color: DS_SEMANTIC.text.regular,
|
|
220
|
+
marginBottom: DS_SPACING[200],
|
|
221
|
+
},
|
|
222
|
+
titleFlex: {
|
|
223
|
+
flex: 1,
|
|
224
|
+
},
|
|
225
|
+
description: {
|
|
226
|
+
...DS_TYPOGRAPHY.bodySm,
|
|
227
|
+
color: DS_SEMANTIC.text.regular,
|
|
228
|
+
},
|
|
229
|
+
descriptionPad: {
|
|
230
|
+
// Start-aligns with the title: headerRow padding (800) + icon + gap (400). Logical start/end
|
|
231
|
+
// (not left/right) so the indent follows the icon+title when the layout mirrors in RTL.
|
|
232
|
+
paddingStart: DS_SPACING[800] + DS_ICON_SIZE.xs + DS_SPACING[400],
|
|
233
|
+
paddingEnd: DS_SPACING[800],
|
|
234
|
+
paddingTop: DS_SPACING[200],
|
|
235
|
+
},
|
|
236
|
+
// Translucent pressed overlay — sits above the banner bg so the tinted bg shows through.
|
|
237
|
+
pressOverlay: {
|
|
238
|
+
...StyleSheet.absoluteFillObject,
|
|
239
|
+
backgroundColor: DS_SEMANTIC.interaction.pressed,
|
|
240
|
+
borderRadius: DS_RADIUS.medium,
|
|
241
|
+
},
|
|
242
|
+
// Collapsible description — clips the animated height; the child is pinned absolute (except on
|
|
243
|
+
// its very first in-flow measuring pass) so shrinking the parent never re-measures/corrupts it.
|
|
244
|
+
collapseClip: {
|
|
245
|
+
overflow: 'hidden',
|
|
246
|
+
width: '100%',
|
|
247
|
+
},
|
|
248
|
+
collapseHidden: {
|
|
249
|
+
height: 0,
|
|
250
|
+
},
|
|
251
|
+
collapseAbsolute: {
|
|
252
|
+
position: 'absolute',
|
|
253
|
+
left: 0,
|
|
254
|
+
right: 0,
|
|
255
|
+
top: 0,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
import type { IconName } from '../../icons';
|
|
3
|
+
|
|
4
|
+
export type BannerVariant = 'neutral' | 'info' | 'positive' | 'warning' | 'negative';
|
|
5
|
+
|
|
6
|
+
export interface BannerProps {
|
|
7
|
+
/** Semantic color scheme. */
|
|
8
|
+
variant?: BannerVariant;
|
|
9
|
+
/** Header text. */
|
|
10
|
+
title?: string;
|
|
11
|
+
/** Body text. */
|
|
12
|
+
description?: string;
|
|
13
|
+
/** When true, the header row (title + chevron) toggles the description open/closed. */
|
|
14
|
+
collapsible?: boolean;
|
|
15
|
+
/** Initial expanded state when collapsible. Defaults to true (description shown). */
|
|
16
|
+
defaultExpanded?: boolean;
|
|
17
|
+
/** Override the variant background color. */
|
|
18
|
+
backgroundColor?: string;
|
|
19
|
+
/** Makes the whole banner tappable, with a pressed state — the tap target is the whole card. */
|
|
20
|
+
onPress?: () => void;
|
|
21
|
+
/** Trailing icon in the header, right-aligned (e.g. 'chevron-right'). */
|
|
22
|
+
trailingIcon?: IconName;
|
|
23
|
+
/** Override the icon/text color (defaults to the variant's emphasis color). */
|
|
24
|
+
textColor?: string;
|
|
25
|
+
/** Override the leading icon for the standard callout layout. */
|
|
26
|
+
icon?: IconName;
|
|
27
|
+
/** Inline tappable link appended to the description text. */
|
|
28
|
+
link?: {
|
|
29
|
+
label: string;
|
|
30
|
+
onPress: () => void;
|
|
31
|
+
};
|
|
32
|
+
/** Action button rendered below the description, right-aligned. */
|
|
33
|
+
action?: {
|
|
34
|
+
label: string;
|
|
35
|
+
onPress: () => void;
|
|
36
|
+
};
|
|
37
|
+
style?: StyleProp<ViewStyle>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { defineCatalogPage, PhoneFrame } from '@krapwoo/ds-viewer';
|
|
4
|
+
import { Button } from '../Button';
|
|
5
|
+
import { Dock } from '../Dock';
|
|
6
|
+
import { TopNav } from '../TopNav';
|
|
7
|
+
import { BottomSheet } from './BottomSheet';
|
|
8
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
|
|
9
|
+
|
|
10
|
+
const styles = StyleSheet.create({
|
|
11
|
+
// BottomSheet/Dropdown demos render inside the shared `PhoneFrame` (./PhoneFrame.tsx) so their
|
|
12
|
+
// absolute overlays stay contained instead of covering the whole catalog page. No padding here —
|
|
13
|
+
// BottomSheet's own content area already supplies the standard 16px.
|
|
14
|
+
sheetContent: {},
|
|
15
|
+
cardTitle: { ...DS_TYPOGRAPHY.labelMd, color: DS_SEMANTIC.text.regular },
|
|
16
|
+
cardBody: { ...DS_TYPOGRAPHY.bodySm, color: DS_SEMANTIC.text.muted, marginTop: DS_SPACING[200] },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function BottomSheetDemo() {
|
|
20
|
+
const [visible, setVisible] = useState(false);
|
|
21
|
+
return (
|
|
22
|
+
<PhoneFrame>
|
|
23
|
+
{!visible && <Button label="Open sheet" onPress={() => setVisible(true)} />}
|
|
24
|
+
<BottomSheet
|
|
25
|
+
visible={visible}
|
|
26
|
+
onDismiss={() => setVisible(false)}
|
|
27
|
+
header={
|
|
28
|
+
<TopNav
|
|
29
|
+
title="Trip details"
|
|
30
|
+
trailing={
|
|
31
|
+
<Button
|
|
32
|
+
variant="secondary"
|
|
33
|
+
size="small"
|
|
34
|
+
showIcon
|
|
35
|
+
showLabel={false}
|
|
36
|
+
iconName="clear"
|
|
37
|
+
accessibilityLabel="Close"
|
|
38
|
+
onPress={() => setVisible(false)}
|
|
39
|
+
/>
|
|
40
|
+
}
|
|
41
|
+
/>
|
|
42
|
+
}
|
|
43
|
+
footer={
|
|
44
|
+
<Dock>
|
|
45
|
+
<Button label="Confirm" onPress={() => setVisible(false)} />
|
|
46
|
+
</Dock>
|
|
47
|
+
}
|
|
48
|
+
>
|
|
49
|
+
<View style={styles.sheetContent}>
|
|
50
|
+
<Text style={styles.cardTitle}>Uptown & The Bronx</Text>
|
|
51
|
+
<Text style={styles.cardBody}>Next train in 4 min · every 6–8 min.</Text>
|
|
52
|
+
{/* Long enough to overflow the phone frame's fixed height — demonstrates the Dock footer's
|
|
53
|
+
`elevated` shadow turning on automatically once this content area actually scrolls. */}
|
|
54
|
+
<Text style={styles.cardBody}>Board at the front car for a faster transfer at Union Sq.</Text>
|
|
55
|
+
<Text style={styles.cardBody}>
|
|
56
|
+
This line runs express between 96 St and 168 St during rush hours, skipping local stops in
|
|
57
|
+
between. Weekend service runs local along the full route, with some stations closed for
|
|
58
|
+
planned maintenance.
|
|
59
|
+
</Text>
|
|
60
|
+
<Text style={styles.cardBody}>
|
|
61
|
+
Elevators are available at 168 St, 137 St, and 125 St — check the map for accessible
|
|
62
|
+
entrances before you travel.
|
|
63
|
+
</Text>
|
|
64
|
+
</View>
|
|
65
|
+
</BottomSheet>
|
|
66
|
+
</PhoneFrame>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default defineCatalogPage({
|
|
71
|
+
component: 'BottomSheet',
|
|
72
|
+
group: 'Navigation',
|
|
73
|
+
description: 'A sheet that slides up over a dismissible backdrop. Height is driven by its content (not fixed snap points) up to 90% of the available height, then the content area scrolls. Compose header with TopNav and footer with Dock.',
|
|
74
|
+
whenToUse: 'For longer, browsable content, or anything that benefits from a TopNav/Dock header-footer structure. For a short, focused decision that interrupts the flow, use Dialog.',
|
|
75
|
+
a11y: 'The backdrop is a Pressable with accessibilityRole="button" and accessibilityLabel="Dismiss"; header/content/footer carry their own accessibility (e.g. TopNav\'s title as accessibilityRole="header").',
|
|
76
|
+
render: () => <BottomSheetDemo />,
|
|
77
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { createContext, type ReactNode, isValidElement, cloneElement, useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { View, Pressable, ScrollView, Animated, Easing, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_RADIUS, DS_SHADOW, DS_SPACING, DS_MOTION_DURATION, DS_MOTION_EASING } from '../../tokens';
|
|
4
|
+
import { Dock, type DockProps } from '../Dock';
|
|
5
|
+
|
|
6
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
7
|
+
const MAX_HEIGHT_PERCENT = '90%';
|
|
8
|
+
|
|
9
|
+
/** True while rendering inside an already-open BottomSheet's tree (header/children/footer alike).
|
|
10
|
+
* Any component that opens its OWN BottomSheet (e.g. Dropdown) reads this via
|
|
11
|
+
* `useInsideBottomSheetWarning` to dev-warn against stacking two sheets — a sheet already covers
|
|
12
|
+
* the whole screen, so a second one has nowhere sensible to appear and just traps the user between
|
|
13
|
+
* two backdrops. Defaults to `false` for anything rendered outside a BottomSheet entirely. */
|
|
14
|
+
const BottomSheetContext = createContext(false);
|
|
15
|
+
|
|
16
|
+
/** Call this from a component that opens its own BottomSheet (Dropdown, another picker, …) — warns
|
|
17
|
+
* once per render if it's currently nested inside a different, already-open BottomSheet. Console
|
|
18
|
+
* warning only (never throws), matching this codebase's other dev-time consistency checks (e.g.
|
|
19
|
+
* ButtonGroup's `checkConsistency`). */
|
|
20
|
+
export function useInsideBottomSheetWarning(componentName: string): void {
|
|
21
|
+
const insideBottomSheet = useContext(BottomSheetContext);
|
|
22
|
+
if (insideBottomSheet) {
|
|
23
|
+
console.warn(
|
|
24
|
+
`[${componentName}] is opening a BottomSheet from inside another, already-open BottomSheet. Never stack two sheets — restructure so ${componentName} lives outside the outer sheet (e.g. in the screen that opens it), or replace it with an inline control (e.g. SegmentedToggle/Radio) for use inside a sheet.`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BottomSheetProps {
|
|
30
|
+
/** Whether the sheet (and its backdrop) are mounted and shown. */
|
|
31
|
+
visible: boolean;
|
|
32
|
+
/** Called when the backdrop is tapped — the sheet doesn't close itself; the caller decides. */
|
|
33
|
+
onDismiss: () => void;
|
|
34
|
+
/** Top area — typically a TopNav with a close/back action. */
|
|
35
|
+
header?: ReactNode;
|
|
36
|
+
/** Middle content area. Grows with its content up to 90% of the available height, then scrolls. */
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
/** Bottom area — typically a Dock with the sheet's primary actions. When `footer` is a Dock
|
|
39
|
+
* element, its `elevated` prop is driven automatically from whether the content area above it
|
|
40
|
+
* actually overflows (needs to scroll) — no need to pass `elevated` yourself. */
|
|
41
|
+
footer?: ReactNode;
|
|
42
|
+
style?: StyleProp<ViewStyle>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A sheet that slides up from the bottom over a dismissible backdrop. Height is driven by its
|
|
47
|
+
* content (not a fixed set of snap points) up to a cap, past which the content area scrolls. Not
|
|
48
|
+
* portaled through RN's `Modal` — it's a plain absolutely-positioned overlay, so it fills whichever
|
|
49
|
+
* parent View hosts it (RN Views are positioning contexts by default), which also lets the catalog
|
|
50
|
+
* contain a live demo inside a bounded preview frame instead of covering the whole page.
|
|
51
|
+
*/
|
|
52
|
+
export function BottomSheet({ visible, onDismiss, header, children, footer, style }: BottomSheetProps) {
|
|
53
|
+
const progress = useRef(new Animated.Value(0)).current;
|
|
54
|
+
const [mounted, setMounted] = useState(visible);
|
|
55
|
+
// The sheet's own rendered height, measured via onLayout — its closed position is "translated
|
|
56
|
+
// down by exactly its own height" (fully below the visible area), not a small fixed nudge, so the
|
|
57
|
+
// motion genuinely reads as sliding in from off-screen regardless of how tall the content is.
|
|
58
|
+
// Falls back to a generous guess before the first measurement lands, to avoid a jump on first open.
|
|
59
|
+
const [sheetHeight, setSheetHeight] = useState(0);
|
|
60
|
+
// Drives a Dock footer's `elevated` shadow: on only once the content area's actual measured
|
|
61
|
+
// height exceeds what it's laid out to show, i.e. it's genuinely scrollable — not just "there is
|
|
62
|
+
// a Dock", so a short sheet stays flat and only a scrolling one gets the separating shadow.
|
|
63
|
+
const [scrollLayoutHeight, setScrollLayoutHeight] = useState(0);
|
|
64
|
+
const [scrollContentHeight, setScrollContentHeight] = useState(0);
|
|
65
|
+
const contentOverflows = scrollContentHeight > scrollLayoutHeight + 1;
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (visible) setMounted(true);
|
|
69
|
+
// Backdrop opacity still fades (a scrim has no position to slide from); the sheet itself only
|
|
70
|
+
// ever moves via translateY below — no opacity animation on the sheet, just the slide.
|
|
71
|
+
const anim = Animated.timing(progress, {
|
|
72
|
+
toValue: visible ? 1 : 0,
|
|
73
|
+
duration: visible ? DS_MOTION_DURATION.base : DS_MOTION_DURATION.fast,
|
|
74
|
+
// `decelerate` — a sheet entering the screen, per DS_MOTION_EASING_USE (previously left on
|
|
75
|
+
// RN's default in-out ease, the one enter/exit overlay without an explicit curve).
|
|
76
|
+
easing: Easing.bezier(...DS_MOTION_EASING.decelerate),
|
|
77
|
+
useNativeDriver: false,
|
|
78
|
+
});
|
|
79
|
+
anim.start(({ finished }) => {
|
|
80
|
+
if (finished && !visible) setMounted(false);
|
|
81
|
+
});
|
|
82
|
+
return () => anim.stop();
|
|
83
|
+
}, [visible, progress]);
|
|
84
|
+
|
|
85
|
+
if (!mounted) return null;
|
|
86
|
+
|
|
87
|
+
const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [sheetHeight || 400, 0] });
|
|
88
|
+
|
|
89
|
+
const resolvedFooter =
|
|
90
|
+
isValidElement(footer) && footer.type === Dock
|
|
91
|
+
? cloneElement(footer as React.ReactElement<DockProps>, { elevated: contentOverflows })
|
|
92
|
+
: footer;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<View style={styles.overlay} pointerEvents={visible ? 'auto' : 'none'}>
|
|
96
|
+
<AnimatedPressable
|
|
97
|
+
style={[styles.backdrop, { opacity: progress }]}
|
|
98
|
+
onPress={onDismiss}
|
|
99
|
+
accessibilityRole="button"
|
|
100
|
+
accessibilityLabel="Dismiss"
|
|
101
|
+
/>
|
|
102
|
+
<Animated.View
|
|
103
|
+
onLayout={(e) => setSheetHeight(e.nativeEvent.layout.height)}
|
|
104
|
+
style={[styles.sheet, style, { transform: [{ translateY }] }]}
|
|
105
|
+
// Contain assistive-tech focus while open (iOS) — without this, content behind the scrim
|
|
106
|
+
// stays reachable by screen readers even though it's visually blocked.
|
|
107
|
+
accessibilityViewIsModal
|
|
108
|
+
>
|
|
109
|
+
<View style={styles.handle} />
|
|
110
|
+
<BottomSheetContext.Provider value>
|
|
111
|
+
{header}
|
|
112
|
+
<ScrollView
|
|
113
|
+
style={styles.content}
|
|
114
|
+
contentContainerStyle={styles.contentContainer}
|
|
115
|
+
bounces={false}
|
|
116
|
+
onLayout={(e) => setScrollLayoutHeight(e.nativeEvent.layout.height)}
|
|
117
|
+
onContentSizeChange={(_width, height) => setScrollContentHeight(height)}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</ScrollView>
|
|
121
|
+
{resolvedFooter}
|
|
122
|
+
</BottomSheetContext.Provider>
|
|
123
|
+
</Animated.View>
|
|
124
|
+
</View>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const styles = StyleSheet.create({
|
|
129
|
+
overlay: { ...StyleSheet.absoluteFillObject, justifyContent: 'flex-end' },
|
|
130
|
+
backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: DS_SEMANTIC.element.overlayBackdrop },
|
|
131
|
+
sheet: {
|
|
132
|
+
maxHeight: MAX_HEIGHT_PERCENT,
|
|
133
|
+
backgroundColor: DS_SEMANTIC.surface.white,
|
|
134
|
+
borderTopLeftRadius: DS_RADIUS.large,
|
|
135
|
+
borderTopRightRadius: DS_RADIUS.large,
|
|
136
|
+
...DS_SHADOW.bottomSheet,
|
|
137
|
+
},
|
|
138
|
+
// Purely a visual affordance (this sheet has no drag-to-dismiss gesture wired up) — signals "you
|
|
139
|
+
// can swipe this" the same way native sheets do, sized to the common ~36×4 grabber convention.
|
|
140
|
+
handle: {
|
|
141
|
+
alignSelf: 'center',
|
|
142
|
+
width: 36,
|
|
143
|
+
height: 4,
|
|
144
|
+
borderRadius: DS_RADIUS.round,
|
|
145
|
+
backgroundColor: DS_SEMANTIC.border.light,
|
|
146
|
+
marginTop: DS_SPACING[300],
|
|
147
|
+
marginBottom: DS_SPACING[100],
|
|
148
|
+
},
|
|
149
|
+
content: { flexGrow: 0 },
|
|
150
|
+
contentContainer: { padding: DS_SPACING[800] },
|
|
151
|
+
});
|