@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,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
4
|
+
import { Card } from './Card';
|
|
5
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
|
|
6
|
+
|
|
7
|
+
const styles = StyleSheet.create({
|
|
8
|
+
cardTitle: { ...DS_TYPOGRAPHY.labelMd, color: DS_SEMANTIC.text.regular },
|
|
9
|
+
cardBody: { ...DS_TYPOGRAPHY.bodySm, color: DS_SEMANTIC.text.muted, marginTop: DS_SPACING[200] },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default defineCatalogPage({
|
|
13
|
+
component: 'Card',
|
|
14
|
+
group: 'Surfaces',
|
|
15
|
+
description: 'The primary content surface — a rounded white card with a soft resting shadow. Pass children, or onPress to make the whole card a button.',
|
|
16
|
+
whenToUse: 'A standalone, self-contained unit with its own shadow. For a row in a homogeneous stack of peers (with dividers between them), use ListItem inside a List instead.',
|
|
17
|
+
a11y: 'When onPress is set, renders a Pressable with accessibilityRole="button" and a visible focus ring; a plain card is a non-interactive View.',
|
|
18
|
+
variants: {
|
|
19
|
+
itemsFill: true,
|
|
20
|
+
items: [
|
|
21
|
+
{
|
|
22
|
+
key: 'default',
|
|
23
|
+
name: 'Default',
|
|
24
|
+
node: (
|
|
25
|
+
<Card>
|
|
26
|
+
<Text style={styles.cardTitle}>Uptown & The Bronx</Text>
|
|
27
|
+
<Text style={styles.cardBody}>Next train in 4 min · every 6–8 min</Text>
|
|
28
|
+
</Card>
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
states: {
|
|
34
|
+
itemsFill: true,
|
|
35
|
+
items: [
|
|
36
|
+
{
|
|
37
|
+
key: 'plain',
|
|
38
|
+
name: 'Plain',
|
|
39
|
+
node: (
|
|
40
|
+
<Card>
|
|
41
|
+
<Text style={styles.cardTitle}>Uptown & The Bronx</Text>
|
|
42
|
+
<Text style={styles.cardBody}>Next train in 4 min · every 6–8 min</Text>
|
|
43
|
+
</Card>
|
|
44
|
+
),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'pressable',
|
|
48
|
+
name: 'Pressable',
|
|
49
|
+
node: (
|
|
50
|
+
<Card onPress={() => {}}>
|
|
51
|
+
<Text style={styles.cardTitle}>Crosstown Bus M14</Text>
|
|
52
|
+
<Text style={styles.cardBody}>Tap to view live arrivals</Text>
|
|
53
|
+
</Card>
|
|
54
|
+
),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: 'disabled',
|
|
58
|
+
name: 'Disabled',
|
|
59
|
+
node: (
|
|
60
|
+
<Card onPress={() => {}} disabled>
|
|
61
|
+
<Text style={styles.cardTitle}>Franklin Ave Shuttle</Text>
|
|
62
|
+
<Text style={styles.cardBody}>Temporarily unavailable</Text>
|
|
63
|
+
</Card>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
type AccessibilityState,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { DS_SEMANTIC, DS_RADIUS, DS_SPACING, DS_SHADOW } from '../../tokens';
|
|
11
|
+
|
|
12
|
+
// Dev-only fill for the Figma slot placeholders (shown when no `children` are passed). Deliberately
|
|
13
|
+
// vivid so it always reads as scaffolding, never as a real surface — not a themed token.
|
|
14
|
+
const DEV_PLACEHOLDER_BG = '#f4c3ff';
|
|
15
|
+
|
|
16
|
+
interface CardBaseProps {
|
|
17
|
+
/** Card content. */
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
/** Figma slot visibility toggles — prefer `children` for real content. */
|
|
20
|
+
showSlot1?: boolean;
|
|
21
|
+
showSlot2?: boolean;
|
|
22
|
+
showSlot3?: boolean;
|
|
23
|
+
showSlot4?: boolean;
|
|
24
|
+
showSlot5?: boolean;
|
|
25
|
+
accessibilityLabel?: string;
|
|
26
|
+
accessibilityState?: AccessibilityState;
|
|
27
|
+
style?: StyleProp<ViewStyle>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// `disabled` only means something on a pressable card — a plain (non-`onPress`) card can't be
|
|
31
|
+
// disabled, so that combination is a type error here instead of a silently-ignored prop.
|
|
32
|
+
export type CardProps =
|
|
33
|
+
| (CardBaseProps & {
|
|
34
|
+
/** Makes the whole card a tappable button. */
|
|
35
|
+
onPress: () => void;
|
|
36
|
+
/** Only valid on a pressable card. */
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
})
|
|
39
|
+
| (CardBaseProps & { onPress?: undefined; disabled?: undefined });
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The primary content surface: a rounded white card with a soft resting shadow. Pass `children`
|
|
43
|
+
* for real content, or `onPress` to make the whole card a tappable button (pressed + focus states).
|
|
44
|
+
*/
|
|
45
|
+
export function Card({
|
|
46
|
+
children,
|
|
47
|
+
showSlot1 = true,
|
|
48
|
+
showSlot2 = true,
|
|
49
|
+
showSlot3 = true,
|
|
50
|
+
showSlot4 = true,
|
|
51
|
+
showSlot5 = true,
|
|
52
|
+
onPress,
|
|
53
|
+
disabled = false,
|
|
54
|
+
accessibilityLabel,
|
|
55
|
+
accessibilityState,
|
|
56
|
+
style,
|
|
57
|
+
}: CardProps) {
|
|
58
|
+
const [focused, setFocused] = React.useState(false);
|
|
59
|
+
const cardStyle = ({ pressed }: { pressed: boolean }) => [
|
|
60
|
+
styles.card,
|
|
61
|
+
pressed && !disabled && styles.cardOnTap,
|
|
62
|
+
focused && !disabled && styles.cardFocused,
|
|
63
|
+
disabled && styles.cardDisabled,
|
|
64
|
+
style,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const inner = children ?? renderSlots(showSlot1, showSlot2, showSlot3, showSlot4, showSlot5);
|
|
68
|
+
|
|
69
|
+
if (onPress) {
|
|
70
|
+
return (
|
|
71
|
+
<Pressable
|
|
72
|
+
onPress={onPress}
|
|
73
|
+
disabled={disabled}
|
|
74
|
+
accessibilityRole="button"
|
|
75
|
+
accessibilityLabel={accessibilityLabel}
|
|
76
|
+
accessibilityState={{ ...accessibilityState, disabled }}
|
|
77
|
+
onFocus={() => setFocused(true)}
|
|
78
|
+
onBlur={() => setFocused(false)}
|
|
79
|
+
style={cardStyle}
|
|
80
|
+
>
|
|
81
|
+
{inner}
|
|
82
|
+
</Pressable>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<View
|
|
88
|
+
style={[styles.card, style]}
|
|
89
|
+
accessible={!!accessibilityLabel}
|
|
90
|
+
accessibilityLabel={accessibilityLabel}
|
|
91
|
+
accessibilityState={accessibilityState}
|
|
92
|
+
>
|
|
93
|
+
{inner}
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderSlots(
|
|
99
|
+
showSlot1: boolean,
|
|
100
|
+
showSlot2: boolean,
|
|
101
|
+
showSlot3: boolean,
|
|
102
|
+
showSlot4: boolean,
|
|
103
|
+
showSlot5: boolean,
|
|
104
|
+
) {
|
|
105
|
+
const slots = [showSlot1, showSlot2, showSlot3, showSlot4, showSlot5].filter(Boolean);
|
|
106
|
+
return slots.map((_, index) => <View key={index} style={styles.placeholder} />);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const styles = StyleSheet.create({
|
|
110
|
+
card: {
|
|
111
|
+
backgroundColor: DS_SEMANTIC.surface.white,
|
|
112
|
+
borderRadius: DS_RADIUS.medium,
|
|
113
|
+
padding: DS_SPACING[800],
|
|
114
|
+
width: '100%',
|
|
115
|
+
...DS_SHADOW.resting,
|
|
116
|
+
},
|
|
117
|
+
cardOnTap: {
|
|
118
|
+
backgroundColor: DS_SEMANTIC.interaction.pressed,
|
|
119
|
+
},
|
|
120
|
+
cardFocused: {
|
|
121
|
+
borderWidth: 2,
|
|
122
|
+
borderColor: DS_SEMANTIC.interaction.focused,
|
|
123
|
+
},
|
|
124
|
+
cardDisabled: {
|
|
125
|
+
opacity: DS_SEMANTIC.interaction.disabledOpacity,
|
|
126
|
+
},
|
|
127
|
+
placeholder: {
|
|
128
|
+
height: 59,
|
|
129
|
+
backgroundColor: DEV_PLACEHOLDER_BG,
|
|
130
|
+
borderRadius: DS_RADIUS.xs,
|
|
131
|
+
width: '100%',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Checkbox } from './Checkbox';
|
|
4
|
+
|
|
5
|
+
function CheckboxDemo() {
|
|
6
|
+
const [checked, setChecked] = useState(true);
|
|
7
|
+
return <Checkbox checked={checked} onChange={setChecked} label="Remember this trip" />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default defineCatalogPage({
|
|
11
|
+
component: 'Checkbox',
|
|
12
|
+
group: 'Controls',
|
|
13
|
+
specimenSize: 'compact',
|
|
14
|
+
description: 'A square selection control — the box fills with the accent colour and a checkmark when checked.',
|
|
15
|
+
whenToUse: 'An independent on/off fact about this one item — any number can be checked at once. For a setting that takes effect immediately, use Switch; for one-of-many exclusive selection, use Radio.',
|
|
16
|
+
a11y: 'Renders a Pressable with accessibilityRole="checkbox" and accessibilityState.checked; the optional label doubles as its accessibilityLabel.',
|
|
17
|
+
variants: {
|
|
18
|
+
items: [{ key: 'default', name: 'Default (tap to toggle)', node: <CheckboxDemo /> }],
|
|
19
|
+
},
|
|
20
|
+
states: {
|
|
21
|
+
items: [
|
|
22
|
+
{ key: 'unchecked', name: 'Unchecked', node: <Checkbox checked={false} onChange={() => {}} label="Remember this trip" /> },
|
|
23
|
+
{ key: 'checked', name: 'Checked', node: <Checkbox checked={true} onChange={() => {}} label="Remember this trip" /> },
|
|
24
|
+
{ key: 'no-label', name: 'No label', node: <Checkbox checked={true} onChange={() => {}} /> },
|
|
25
|
+
{ key: 'disabled', name: 'Disabled', node: <Checkbox checked={true} onChange={() => {}} disabled label="Remember this trip" /> },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Pressable, Text, View, Animated, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_SPACING, DS_RADIUS, DS_TYPOGRAPHY, DS_A11Y_MIN_TOUCH_TARGET, DS_MOTION_DURATION, DS_ICON_SIZE } from '../../tokens';
|
|
4
|
+
import { Icon } from '../../icons/Icon.native';
|
|
5
|
+
import { usePressScale } from '../Radio/usePressScale';
|
|
6
|
+
|
|
7
|
+
export interface CheckboxProps {
|
|
8
|
+
/** Whether the box is checked. */
|
|
9
|
+
checked: boolean;
|
|
10
|
+
/** Called with the new value on tap. */
|
|
11
|
+
onChange: (checked: boolean) => void;
|
|
12
|
+
/** Optional inline label rendered to the right of the box. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Non-interactive, dimmed. */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
style?: StyleProp<ViewStyle>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const BOX_SIZE = 20;
|
|
20
|
+
const ANIM_MS = DS_MOTION_DURATION.fast;
|
|
21
|
+
// Pads the tappable area out to the accessibility minimum without growing the visual box — same
|
|
22
|
+
// reasoning as Button's own `iconOnlyHitSlop` and Radio's `HIT_SLOP`.
|
|
23
|
+
const HIT_SLOP = Math.max(0, Math.ceil((DS_A11Y_MIN_TOUCH_TARGET - BOX_SIZE) / 2));
|
|
24
|
+
|
|
25
|
+
/** A square selection control — the box fills with the accent colour and the checkmark pops in
|
|
26
|
+
* (scale + opacity) when checked, and back out when unchecked — kept JS-driven
|
|
27
|
+
* (`useNativeDriver: false`) since the box's own border/background colour animate too. The box
|
|
28
|
+
* also grows slightly while pressed, before release — the same tactile feedback Radio and Switch
|
|
29
|
+
* already give, so all three selection controls feel consistent. */
|
|
30
|
+
export function Checkbox({ checked, onChange, label, disabled = false, style }: CheckboxProps) {
|
|
31
|
+
const checkAnim = useRef(new Animated.Value(checked ? 1 : 0)).current;
|
|
32
|
+
const { pressScale, onPressIn, onPressOut } = usePressScale(disabled);
|
|
33
|
+
const [focused, setFocused] = useState(false);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const anim = Animated.timing(checkAnim, { toValue: checked ? 1 : 0, duration: ANIM_MS, useNativeDriver: false });
|
|
37
|
+
anim.start();
|
|
38
|
+
return () => anim.stop();
|
|
39
|
+
}, [checked, checkAnim]);
|
|
40
|
+
|
|
41
|
+
const boxBackground = checkAnim.interpolate({ inputRange: [0, 1], outputRange: ['rgba(0,0,0,0)', DS_SEMANTIC.emphasis.info] });
|
|
42
|
+
const boxBorderColor = checkAnim.interpolate({ inputRange: [0, 1], outputRange: [DS_SEMANTIC.border.dark, DS_SEMANTIC.emphasis.info] });
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Pressable
|
|
46
|
+
onPress={() => !disabled && onChange(!checked)}
|
|
47
|
+
onPressIn={onPressIn}
|
|
48
|
+
onPressOut={onPressOut}
|
|
49
|
+
onFocus={() => setFocused(true)}
|
|
50
|
+
onBlur={() => setFocused(false)}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
accessibilityRole="checkbox"
|
|
53
|
+
accessibilityState={{ checked, disabled }}
|
|
54
|
+
accessibilityLabel={label}
|
|
55
|
+
hitSlop={HIT_SLOP}
|
|
56
|
+
style={[styles.row, disabled && styles.disabled, style]}
|
|
57
|
+
>
|
|
58
|
+
<Animated.View style={[styles.box, { backgroundColor: boxBackground, borderColor: boxBorderColor, transform: [{ scale: pressScale }] }]}>
|
|
59
|
+
<Animated.View style={{ opacity: checkAnim, transform: [{ scale: checkAnim }] }}>
|
|
60
|
+
<Icon name="check" size={DS_ICON_SIZE.xs} color={DS_SEMANTIC.surface.white} strokeWidth={3} />
|
|
61
|
+
</Animated.View>
|
|
62
|
+
{/* Keyboard-focus ring — the same interaction.focused treatment Button/Pill use, drawn as a
|
|
63
|
+
zero-layout absolute overlay around the box so focusing never shifts layout. */}
|
|
64
|
+
{focused && !disabled && <View pointerEvents="none" style={styles.focusRing} />}
|
|
65
|
+
</Animated.View>
|
|
66
|
+
{!!label && <Text style={styles.label}>{label}</Text>}
|
|
67
|
+
</Pressable>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const styles = StyleSheet.create({
|
|
72
|
+
row: {
|
|
73
|
+
flexDirection: 'row',
|
|
74
|
+
alignItems: 'center',
|
|
75
|
+
gap: DS_SPACING[400],
|
|
76
|
+
},
|
|
77
|
+
box: {
|
|
78
|
+
width: BOX_SIZE,
|
|
79
|
+
height: BOX_SIZE,
|
|
80
|
+
borderRadius: DS_RADIUS.xs,
|
|
81
|
+
borderWidth: 2,
|
|
82
|
+
borderColor: DS_SEMANTIC.border.dark,
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
justifyContent: 'center',
|
|
85
|
+
},
|
|
86
|
+
label: {
|
|
87
|
+
...DS_TYPOGRAPHY.bodyMd,
|
|
88
|
+
color: DS_SEMANTIC.text.regular,
|
|
89
|
+
},
|
|
90
|
+
disabled: {
|
|
91
|
+
opacity: DS_SEMANTIC.interaction.disabledOpacity,
|
|
92
|
+
},
|
|
93
|
+
// 2px air + 2px ring outside the box, radius grown by the same offset to stay concentric.
|
|
94
|
+
focusRing: {
|
|
95
|
+
position: 'absolute',
|
|
96
|
+
top: -4,
|
|
97
|
+
bottom: -4,
|
|
98
|
+
left: -4,
|
|
99
|
+
right: -4,
|
|
100
|
+
borderWidth: 2,
|
|
101
|
+
borderColor: DS_SEMANTIC.interaction.focused,
|
|
102
|
+
borderRadius: DS_RADIUS.xs + 4,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
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 { ButtonGroup } from '../ButtonGroup';
|
|
6
|
+
import { Dialog } from './Dialog';
|
|
7
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
|
|
8
|
+
|
|
9
|
+
const styles = StyleSheet.create({
|
|
10
|
+
cardTitle: { ...DS_TYPOGRAPHY.labelMd, color: DS_SEMANTIC.text.regular },
|
|
11
|
+
cardBody: { ...DS_TYPOGRAPHY.bodySm, color: DS_SEMANTIC.text.muted, marginTop: DS_SPACING[200] },
|
|
12
|
+
dialogActions: { marginTop: DS_SPACING[800] },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function DialogDemo() {
|
|
16
|
+
const [visible, setVisible] = useState(false);
|
|
17
|
+
return (
|
|
18
|
+
<PhoneFrame>
|
|
19
|
+
{!visible && <Button label="Open dialog" onPress={() => setVisible(true)} />}
|
|
20
|
+
<Dialog visible={visible} onDismiss={() => setVisible(false)}>
|
|
21
|
+
<Text style={styles.cardTitle}>Delete this trip?</Text>
|
|
22
|
+
<Text style={styles.cardBody}>This can't be undone.</Text>
|
|
23
|
+
<View style={styles.dialogActions}>
|
|
24
|
+
<ButtonGroup>
|
|
25
|
+
<Button label="Cancel" variant="tertiary" onPress={() => setVisible(false)} />
|
|
26
|
+
<Button label="Delete" onPress={() => setVisible(false)} />
|
|
27
|
+
</ButtonGroup>
|
|
28
|
+
</View>
|
|
29
|
+
</Dialog>
|
|
30
|
+
</PhoneFrame>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default defineCatalogPage({
|
|
35
|
+
component: 'Dialog',
|
|
36
|
+
group: 'Overlays',
|
|
37
|
+
description: 'A card centred on screen over a dismissible backdrop — fades and scales in, distinct from BottomSheet\'s bottom-anchored slide. Named Dialog (not Modal) to avoid shadowing React Native\'s own built-in Modal.',
|
|
38
|
+
whenToUse: 'A short, focused decision that interrupts the flow (confirm/cancel, a single form). For anything longer, browsable, or that needs its own internal scrolling, use BottomSheet.',
|
|
39
|
+
a11y: 'The backdrop is a Pressable with accessibilityRole="button" and accessibilityLabel="Dismiss"; content you pass as children carries its own accessibility.',
|
|
40
|
+
render: () => <DialogDemo />,
|
|
41
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { View, Pressable, Animated, Easing, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_SPACING, DS_RADIUS, DS_SHADOW, DS_MOTION_DURATION, DS_MOTION_EASING } from '../../tokens';
|
|
4
|
+
|
|
5
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
6
|
+
|
|
7
|
+
export interface DialogProps {
|
|
8
|
+
/** Whether the dialog (and its backdrop) are mounted and shown. */
|
|
9
|
+
visible: boolean;
|
|
10
|
+
/** Called when the backdrop is tapped — the dialog doesn't close itself; the caller decides. */
|
|
11
|
+
onDismiss: () => void;
|
|
12
|
+
/** The dialog's content. */
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
style?: StyleProp<ViewStyle>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A card centred on screen over a dismissible backdrop — fades and scales in, distinct from
|
|
19
|
+
* {@link BottomSheet}'s bottom-anchored slide. Named `Dialog` (not `Modal`) to avoid shadowing React
|
|
20
|
+
* Native's own built-in `Modal`. Like BottomSheet, not portaled through RN's `Modal` — a plain
|
|
21
|
+
* absolutely-positioned overlay that fills whichever parent View hosts it.
|
|
22
|
+
*/
|
|
23
|
+
export function Dialog({ visible, onDismiss, children, style }: DialogProps) {
|
|
24
|
+
const progress = useRef(new Animated.Value(0)).current;
|
|
25
|
+
const [mounted, setMounted] = useState(visible);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (visible) setMounted(true);
|
|
29
|
+
const anim = Animated.timing(progress, {
|
|
30
|
+
toValue: visible ? 1 : 0,
|
|
31
|
+
duration: visible ? DS_MOTION_DURATION.base : DS_MOTION_DURATION.fast,
|
|
32
|
+
// `decelerate` — something entering the screen, per DS_MOTION_EASING_USE.
|
|
33
|
+
easing: Easing.bezier(...DS_MOTION_EASING.decelerate),
|
|
34
|
+
useNativeDriver: false,
|
|
35
|
+
});
|
|
36
|
+
anim.start(({ finished }) => {
|
|
37
|
+
if (finished && !visible) setMounted(false);
|
|
38
|
+
});
|
|
39
|
+
return () => anim.stop();
|
|
40
|
+
}, [visible, progress]);
|
|
41
|
+
|
|
42
|
+
if (!mounted) return null;
|
|
43
|
+
|
|
44
|
+
const scale = progress.interpolate({ inputRange: [0, 1], outputRange: [0.92, 1] });
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View style={styles.overlay} pointerEvents={visible ? 'auto' : 'none'}>
|
|
48
|
+
<AnimatedPressable
|
|
49
|
+
style={[styles.backdrop, { opacity: progress }]}
|
|
50
|
+
onPress={onDismiss}
|
|
51
|
+
accessibilityRole="button"
|
|
52
|
+
accessibilityLabel="Dismiss"
|
|
53
|
+
/>
|
|
54
|
+
<Animated.View
|
|
55
|
+
style={[styles.card, style, { opacity: progress, transform: [{ scale }] }]}
|
|
56
|
+
// Contain assistive-tech focus while open (iOS) — without this, content behind the scrim
|
|
57
|
+
// stays reachable by screen readers even though it's visually blocked.
|
|
58
|
+
accessibilityViewIsModal
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</Animated.View>
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const styles = StyleSheet.create({
|
|
67
|
+
overlay: {
|
|
68
|
+
...StyleSheet.absoluteFillObject,
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
padding: DS_SPACING[800],
|
|
72
|
+
},
|
|
73
|
+
backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: DS_SEMANTIC.element.overlayBackdrop },
|
|
74
|
+
card: {
|
|
75
|
+
width: '100%',
|
|
76
|
+
maxWidth: 400,
|
|
77
|
+
backgroundColor: DS_SEMANTIC.surface.white,
|
|
78
|
+
borderRadius: DS_RADIUS.large,
|
|
79
|
+
padding: DS_SPACING[800],
|
|
80
|
+
...DS_SHADOW.card,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
4
|
+
import { Divider } from './Divider';
|
|
5
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
|
|
6
|
+
|
|
7
|
+
const styles = StyleSheet.create({
|
|
8
|
+
cardBody: { ...DS_TYPOGRAPHY.bodySm, color: DS_SEMANTIC.text.muted, marginTop: DS_SPACING[200] },
|
|
9
|
+
dividerDemo: { width: '100%', gap: DS_SPACING[400] },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default defineCatalogPage({
|
|
13
|
+
component: 'Divider',
|
|
14
|
+
group: 'Layout',
|
|
15
|
+
description: 'A 1px hairline separator at the divider token colour.',
|
|
16
|
+
a11y: 'A plain, non-interactive View — purely decorative.',
|
|
17
|
+
// No `variant` prop and no other real configuration exists on Divider — a single hairline is the
|
|
18
|
+
// whole component, shown here between two lines of content to demonstrate real placement.
|
|
19
|
+
variants: {
|
|
20
|
+
itemsFill: true,
|
|
21
|
+
items: [
|
|
22
|
+
{
|
|
23
|
+
key: 'default',
|
|
24
|
+
name: 'Default',
|
|
25
|
+
node: (
|
|
26
|
+
<View style={styles.dividerDemo}>
|
|
27
|
+
<Text style={styles.cardBody}>Section one</Text>
|
|
28
|
+
<Divider />
|
|
29
|
+
<Text style={styles.cardBody}>Section two</Text>
|
|
30
|
+
</View>
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC } from '../../tokens';
|
|
4
|
+
|
|
5
|
+
export interface DividerProps {
|
|
6
|
+
style?: StyleProp<ViewStyle>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A 1px hairline separator at the divider token colour. */
|
|
10
|
+
export function Divider({ style }: DividerProps) {
|
|
11
|
+
return <View style={[styles.line, style]} />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const styles = StyleSheet.create({
|
|
15
|
+
line: {
|
|
16
|
+
height: 1,
|
|
17
|
+
width: '100%',
|
|
18
|
+
backgroundColor: DS_SEMANTIC.element.divider,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Button } from '../Button';
|
|
4
|
+
import { Dock } from './Dock';
|
|
5
|
+
|
|
6
|
+
export default defineCatalogPage({
|
|
7
|
+
component: 'Dock',
|
|
8
|
+
group: 'Navigation',
|
|
9
|
+
description: 'Pinned to the bottom of the screen, above the home indicator — holds up to three full-width Buttons stacked vertically, with an optional small caption area above them.',
|
|
10
|
+
a11y: 'A plain View; each Button child carries its own accessibility role and label.',
|
|
11
|
+
// No `variant` prop exists on Dock — its "Variants" column just shows the one default look,
|
|
12
|
+
// at its max button count; the caption/count variations live under "States".
|
|
13
|
+
variants: {
|
|
14
|
+
itemsFill: true,
|
|
15
|
+
items: [
|
|
16
|
+
{
|
|
17
|
+
key: 'default',
|
|
18
|
+
name: 'Default',
|
|
19
|
+
node: (
|
|
20
|
+
<Dock caption="3 stops · 24 min total">
|
|
21
|
+
<Button label="Start trip" onPress={() => {}} />
|
|
22
|
+
<Button label="Save for later" variant="secondary" onPress={() => {}} />
|
|
23
|
+
<Button label="Share" variant="tertiary" onPress={() => {}} />
|
|
24
|
+
</Dock>
|
|
25
|
+
),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
states: {
|
|
30
|
+
itemsFill: true,
|
|
31
|
+
items: [
|
|
32
|
+
{
|
|
33
|
+
key: 'no-caption',
|
|
34
|
+
name: 'No caption',
|
|
35
|
+
node: (
|
|
36
|
+
<Dock>
|
|
37
|
+
<Button label="Confirm" onPress={() => {}} />
|
|
38
|
+
<Button label="Cancel" variant="tertiary" onPress={() => {}} />
|
|
39
|
+
</Dock>
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: 'caption-hidden',
|
|
44
|
+
name: 'Caption hidden',
|
|
45
|
+
node: (
|
|
46
|
+
<Dock caption="3 stops · 24 min total" showCaption={false}>
|
|
47
|
+
<Button label="Start trip" onPress={() => {}} />
|
|
48
|
+
</Dock>
|
|
49
|
+
),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'single-button',
|
|
53
|
+
name: 'Single button',
|
|
54
|
+
node: (
|
|
55
|
+
<Dock>
|
|
56
|
+
<Button label="Got it" onPress={() => {}} />
|
|
57
|
+
</Dock>
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: 'elevated',
|
|
62
|
+
name: 'Elevated',
|
|
63
|
+
node: (
|
|
64
|
+
<Dock elevated caption="3 stops · 24 min total">
|
|
65
|
+
<Button label="Start trip" onPress={() => {}} />
|
|
66
|
+
</Dock>
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY, DS_SHADOW } from '../../tokens';
|
|
5
|
+
|
|
6
|
+
const MAX_BUTTONS = 3;
|
|
7
|
+
const TOP_PAD = DS_SPACING[800];
|
|
8
|
+
const BOTTOM_PAD = DS_SPACING[400];
|
|
9
|
+
const H_PAD = DS_SPACING[800];
|
|
10
|
+
|
|
11
|
+
export interface DockProps {
|
|
12
|
+
/** Up to three full-width Button elements, stacked vertically. Extra children are dropped. */
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
/** Small caption shown above the top button — e.g. a trip summary. */
|
|
15
|
+
caption?: ReactNode;
|
|
16
|
+
/** Hides the caption row without unmounting `caption`. @default true */
|
|
17
|
+
showCaption?: boolean;
|
|
18
|
+
/** Shows the upward-cast `bottomSheet` shadow, visually separating the Dock from scrollable
|
|
19
|
+
* content sitting above it. Off by default — turn it on when that content actually overflows
|
|
20
|
+
* (is scrolled/scrollable), since Dock has no visibility into sibling content on its own; the
|
|
21
|
+
* caller (e.g. BottomSheet, which measures its own content area) decides when that's true.
|
|
22
|
+
* @default false */
|
|
23
|
+
elevated?: boolean;
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pinned to the bottom of the screen, above the home indicator — holds up to three full-width
|
|
29
|
+
* Buttons stacked vertically, with an optional small caption area above them that can be shown or
|
|
30
|
+
* hidden independently of whether `caption` content is passed. Not absolutely positioned itself —
|
|
31
|
+
* the screen composing this decides whether to pin it (`position: 'absolute', bottom: 0`) or lay
|
|
32
|
+
* it out inline at the end of a flex column, which also keeps it usable inside a bounded container
|
|
33
|
+
* like this catalog's own example cards.
|
|
34
|
+
*/
|
|
35
|
+
export function Dock({ children, caption, showCaption = true, elevated = false, style }: DockProps) {
|
|
36
|
+
const insets = useSafeAreaInsets();
|
|
37
|
+
const buttons = React.Children.toArray(children).slice(0, MAX_BUTTONS);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View style={[styles.dock, elevated && styles.dockElevated, { paddingBottom: BOTTOM_PAD + insets.bottom }, style]}>
|
|
41
|
+
{caption != null && showCaption && <Text style={styles.caption}>{caption}</Text>}
|
|
42
|
+
<View style={styles.buttons}>{buttons}</View>
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const styles = StyleSheet.create({
|
|
48
|
+
dock: {
|
|
49
|
+
width: '100%',
|
|
50
|
+
backgroundColor: DS_SEMANTIC.surface.white,
|
|
51
|
+
paddingTop: TOP_PAD,
|
|
52
|
+
paddingHorizontal: H_PAD,
|
|
53
|
+
gap: DS_SPACING[300],
|
|
54
|
+
},
|
|
55
|
+
dockElevated: { ...DS_SHADOW.bottomSheet },
|
|
56
|
+
caption: { ...DS_TYPOGRAPHY.bodyXs, color: DS_SEMANTIC.text.muted, textAlign: 'center' },
|
|
57
|
+
buttons: { gap: DS_SPACING[400] },
|
|
58
|
+
});
|