@krapwoo/ds-viewer 0.1.0 → 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 +102 -3
- 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 +11 -7
- 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,96 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage, type ComparisonDef } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Badge } from './Badge';
|
|
4
|
+
import type { BadgeVariant } from './Badge.types';
|
|
5
|
+
|
|
6
|
+
// Grid comparisons: each cell is a real instance with exactly the props its row and column name.
|
|
7
|
+
// Never build these by multiplying `variants` with `states` — those are pre-rendered nodes and
|
|
8
|
+
// cannot combine.
|
|
9
|
+
function grid(
|
|
10
|
+
rowLabel: string,
|
|
11
|
+
columnLabel: string,
|
|
12
|
+
rows: { key: string; label: string }[],
|
|
13
|
+
columns: { key: string; label: string }[],
|
|
14
|
+
cell: (row: string, column: string) => React.ReactNode,
|
|
15
|
+
size?: ComparisonDef['size'],
|
|
16
|
+
): ComparisonDef {
|
|
17
|
+
return {
|
|
18
|
+
rowLabel,
|
|
19
|
+
columnLabel,
|
|
20
|
+
rows,
|
|
21
|
+
columns,
|
|
22
|
+
cells: rows.flatMap((row) => columns.map((column) => ({ rowKey: row.key, columnKey: column.key, node: cell(row.key, column.key) }))),
|
|
23
|
+
size,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Badge: Tone × Icon layout. One icon per tone, matching the Variants examples.
|
|
28
|
+
const BADGE_TONES: { key: BadgeVariant; label: string; text: string; icon: React.ComponentProps<typeof Badge>['leadingIcon'] }[] = [
|
|
29
|
+
{ key: 'neutral', label: 'Neutral', text: 'Local', icon: 'pin' },
|
|
30
|
+
{ key: 'info', label: 'Info', text: 'Notice', icon: 'info-circle' },
|
|
31
|
+
{ key: 'positive', label: 'Positive', text: 'On time', icon: 'circle-check' },
|
|
32
|
+
{ key: 'warning', label: 'Warning', text: 'Delayed', icon: 'triangle-alert' },
|
|
33
|
+
{ key: 'negative', label: 'Negative', text: 'Suspended', icon: 'circle-slash' },
|
|
34
|
+
];
|
|
35
|
+
const BADGE_COMPARISON = grid(
|
|
36
|
+
'Tone',
|
|
37
|
+
'Icon',
|
|
38
|
+
BADGE_TONES.map(({ key, label }) => ({ key, label })),
|
|
39
|
+
[
|
|
40
|
+
{ key: 'label-only', label: 'Label only' },
|
|
41
|
+
{ key: 'leading-icon', label: 'Leading icon' },
|
|
42
|
+
{ key: 'trailing-icon', label: 'Trailing icon' },
|
|
43
|
+
{ key: 'icon-only', label: 'Icon-only' },
|
|
44
|
+
],
|
|
45
|
+
(row, column) => {
|
|
46
|
+
const tone = BADGE_TONES.find((t) => t.key === row)!;
|
|
47
|
+
if (column === 'icon-only') return <Badge variant={tone.key} leadingIcon={tone.icon} accessibilityLabel={tone.text} />;
|
|
48
|
+
return (
|
|
49
|
+
<Badge
|
|
50
|
+
variant={tone.key}
|
|
51
|
+
label={tone.text}
|
|
52
|
+
leadingIcon={column === 'leading-icon' ? tone.icon : undefined}
|
|
53
|
+
trailingIcon={column === 'trailing-icon' ? tone.icon : undefined}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
'compact',
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export default defineCatalogPage({
|
|
61
|
+
component: 'Badge',
|
|
62
|
+
group: 'Surfaces',
|
|
63
|
+
specimenSize: 'compact',
|
|
64
|
+
comparison: BADGE_COMPARISON,
|
|
65
|
+
description: 'A small status chip — five semantic variants, with optional leading/trailing icons or icon-only.',
|
|
66
|
+
whenToUse: "Read-only and inline, not tappable — for one row's data point. For a tappable chip with a selected state, use Pill; for a message about the whole screen, use Toast or Banner.",
|
|
67
|
+
a11y: 'A plain View with text; the label carries the meaning, so avoid encoding status by color alone.',
|
|
68
|
+
variants: {
|
|
69
|
+
items: [
|
|
70
|
+
{ key: 'neutral', name: 'Neutral', props: { variant: 'neutral' }, node: <Badge variant="neutral" label="Local" /> },
|
|
71
|
+
{ key: 'info', name: 'Info', props: { variant: 'info' }, node: <Badge variant="info" label="Notice" leadingIcon="info-circle" /> },
|
|
72
|
+
{ key: 'positive', name: 'Positive', props: { variant: 'positive' }, node: <Badge variant="positive" label="On time" leadingIcon="circle-check" /> },
|
|
73
|
+
{ key: 'warning', name: 'Warning', props: { variant: 'warning' }, node: <Badge variant="warning" label="Delayed" leadingIcon="triangle-alert" /> },
|
|
74
|
+
{ key: 'negative', name: 'Negative', props: { variant: 'negative' }, node: <Badge variant="negative" label="Suspended" leadingIcon="circle-slash" /> },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
states: {
|
|
78
|
+
items: [
|
|
79
|
+
{
|
|
80
|
+
key: 'leading-icon',
|
|
81
|
+
name: 'Leading icon',
|
|
82
|
+
node: <Badge variant="positive" label="On time" leadingIcon="circle-check" />,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: 'trailing-icon',
|
|
86
|
+
name: 'Trailing icon',
|
|
87
|
+
node: <Badge variant="positive" label="On time" trailingIcon="circle-check" />,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: 'icon-only',
|
|
91
|
+
name: 'Icon-only',
|
|
92
|
+
node: <Badge variant="info" leadingIcon="info-circle" accessibilityLabel="Notice" />,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { DS_RADIUS, DS_SPACING, DS_TYPOGRAPHY, DS_ICON_SIZE } from '../../tokens';
|
|
4
|
+
import { Icon } from '../../icons/Icon.native';
|
|
5
|
+
import { STATUS_BG, STATUS_FG } from '../statusColors';
|
|
6
|
+
import type { BadgeProps } from './Badge.types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A small status pill. Composable: label-only, icon-only (a single icon, no label), or a label with
|
|
10
|
+
* a leading and/or trailing icon. Text + icons sit on the variant background in the variant's own
|
|
11
|
+
* emphasis colour.
|
|
12
|
+
*/
|
|
13
|
+
export function Badge({ variant = 'neutral', label, leadingIcon, trailingIcon, accessibilityLabel, style }: BadgeProps) {
|
|
14
|
+
const hasLabel = label != null && label !== '';
|
|
15
|
+
// Dev-console check (same policy as ButtonGroup's checkConsistency): the docs require an
|
|
16
|
+
// accessible name for an icon-only badge, but the type can't enforce it — warn instead of
|
|
17
|
+
// silently rendering an unnameable status glyph.
|
|
18
|
+
if (__DEV__ && !hasLabel && !accessibilityLabel) {
|
|
19
|
+
console.warn('[Badge] Icon-only badge (no `label`) needs an `accessibilityLabel` — without one it has no accessible name.');
|
|
20
|
+
}
|
|
21
|
+
const fg = STATUS_FG[variant];
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View
|
|
25
|
+
style={[styles.badge, !hasLabel && styles.iconOnly, { backgroundColor: STATUS_BG[variant] }, style]}
|
|
26
|
+
accessibilityRole="text"
|
|
27
|
+
accessibilityLabel={accessibilityLabel}
|
|
28
|
+
>
|
|
29
|
+
{leadingIcon ? <Icon name={leadingIcon} size={DS_ICON_SIZE.xs} color={fg} /> : null}
|
|
30
|
+
{hasLabel ? (
|
|
31
|
+
<Text style={[styles.label, { color: fg }]} numberOfLines={1}>
|
|
32
|
+
{label}
|
|
33
|
+
</Text>
|
|
34
|
+
) : null}
|
|
35
|
+
{trailingIcon ? <Icon name={trailingIcon} size={DS_ICON_SIZE.xs} color={fg} /> : null}
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const styles = StyleSheet.create({
|
|
41
|
+
badge: {
|
|
42
|
+
flexDirection: 'row',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
gap: DS_SPACING[100],
|
|
45
|
+
paddingVertical: DS_SPACING[200],
|
|
46
|
+
paddingHorizontal: DS_SPACING[400],
|
|
47
|
+
borderRadius: DS_RADIUS.round,
|
|
48
|
+
alignSelf: 'flex-start',
|
|
49
|
+
},
|
|
50
|
+
iconOnly: {
|
|
51
|
+
paddingHorizontal: DS_SPACING[200],
|
|
52
|
+
},
|
|
53
|
+
label: {
|
|
54
|
+
...DS_TYPOGRAPHY.labelXs,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -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
|
+
});
|