@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,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
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { defineCatalogPage, PhoneFrame } from '@krapwoo/ds-viewer';
|
|
4
|
+
import { Dropdown } from './Dropdown';
|
|
5
|
+
import { DS_SPACING } from '../../tokens';
|
|
6
|
+
|
|
7
|
+
const styles = StyleSheet.create({
|
|
8
|
+
// Dropdown demo — fills PhoneFrame edge-to-edge (`flex:1` + `alignSelf:'stretch'` override
|
|
9
|
+
// PhoneFrame's own `alignItems/justifyContent:'center'`, which centers-and-shrinks BottomSheetDemo's
|
|
10
|
+
// trigger button just fine but would otherwise leave this wrapper short and centered too). Dropdown's
|
|
11
|
+
// own BottomSheet is a sibling of its trigger in the same tree position, so its absolute overlay
|
|
12
|
+
// fills *this* box — it needs to be the full frame, not a small box hugging just the trigger.
|
|
13
|
+
dropdownFrameContent: { flex: 1, alignSelf: 'stretch', padding: DS_SPACING[800] },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const SAMPLE_DROPDOWN_OPTIONS = [
|
|
17
|
+
{ value: 'uptown', label: 'Uptown & The Bronx' },
|
|
18
|
+
{ value: 'downtown', label: 'Downtown & Brooklyn' },
|
|
19
|
+
{ value: 'crosstown', label: 'Crosstown' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function DropdownDemo() {
|
|
23
|
+
const [value, setValue] = useState('uptown');
|
|
24
|
+
return (
|
|
25
|
+
<PhoneFrame>
|
|
26
|
+
<View style={styles.dropdownFrameContent}>
|
|
27
|
+
<Dropdown
|
|
28
|
+
label="Direction"
|
|
29
|
+
value={value}
|
|
30
|
+
onChange={setValue}
|
|
31
|
+
placeholder="Choose a direction"
|
|
32
|
+
options={SAMPLE_DROPDOWN_OPTIONS}
|
|
33
|
+
/>
|
|
34
|
+
</View>
|
|
35
|
+
</PhoneFrame>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default defineCatalogPage({
|
|
40
|
+
component: 'Dropdown',
|
|
41
|
+
group: 'Inputs',
|
|
42
|
+
description: 'A labelled field that opens a BottomSheet picker on tap — the "bottom sheet picker" interaction: tap the trigger, pick an option from the sheet, it closes.',
|
|
43
|
+
whenToUse: 'The answer is one of a small, known set of choices — never free text. For free-text filtering, use SearchField; for a named field with a fixed identity, use InputField.',
|
|
44
|
+
a11y: 'The trigger is a Pressable (via FieldContainer) with accessibilityRole="button". Options render as accessibilityRole="radio" inside a "radiogroup", with accessibilityState.checked reflecting the current selection.',
|
|
45
|
+
variants: {
|
|
46
|
+
itemsFill: true,
|
|
47
|
+
items: [{ key: 'default', name: 'Default (tap to open)', node: <DropdownDemo /> }],
|
|
48
|
+
},
|
|
49
|
+
states: {
|
|
50
|
+
itemsFill: true,
|
|
51
|
+
items: [
|
|
52
|
+
{
|
|
53
|
+
key: 'placeholder',
|
|
54
|
+
name: 'Placeholder',
|
|
55
|
+
node: <Dropdown label="Direction" placeholder="Choose a direction" options={SAMPLE_DROPDOWN_OPTIONS} onChange={() => {}} />,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: 'no-label',
|
|
59
|
+
name: 'No label',
|
|
60
|
+
node: <Dropdown value="uptown" options={SAMPLE_DROPDOWN_OPTIONS} onChange={() => {}} />,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: 'disabled',
|
|
64
|
+
name: 'Disabled',
|
|
65
|
+
node: <Dropdown label="Direction" value="uptown" options={SAMPLE_DROPDOWN_OPTIONS} onChange={() => {}} disabled />,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
});
|