@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,33 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Switch } from './Switch';
|
|
4
|
+
|
|
5
|
+
function SwitchDemo() {
|
|
6
|
+
const [value, setValue] = useState(true);
|
|
7
|
+
return <Switch value={value} onValueChange={setValue} accessibilityLabel="Notifications" />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function SwitchLabelDemo() {
|
|
11
|
+
const [value, setValue] = useState(true);
|
|
12
|
+
return <Switch value={value} onValueChange={setValue} label="Notifications" />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default defineCatalogPage({
|
|
16
|
+
component: 'Switch',
|
|
17
|
+
group: 'Controls',
|
|
18
|
+
specimenSize: 'compact',
|
|
19
|
+
description: 'A boolean on/off toggle. The thumb slides and the track crossfades colour, sharing SegmentedToggle/UnderlineTabs\' own slide-animation hook for a consistent motion feel.',
|
|
20
|
+
whenToUse: 'A setting that takes effect immediately, no separate save step. For recording a fact a future action (like a form submit) will act on, use Checkbox; for one-of-many exclusive selection, use Radio.',
|
|
21
|
+
a11y: 'Renders a Pressable with accessibilityRole="switch" and accessibilityState.checked — pass accessibilityLabel to say what it controls.',
|
|
22
|
+
variants: {
|
|
23
|
+
items: [{ key: 'default', name: 'Default (tap to toggle)', node: <SwitchDemo /> }],
|
|
24
|
+
},
|
|
25
|
+
states: {
|
|
26
|
+
items: [
|
|
27
|
+
{ key: 'off', name: 'Off', node: <Switch value={false} onValueChange={() => {}} accessibilityLabel="Notifications" /> },
|
|
28
|
+
{ key: 'on', name: 'On', node: <Switch value={true} onValueChange={() => {}} accessibilityLabel="Notifications" /> },
|
|
29
|
+
{ key: 'label', name: 'With label', props: { label: true }, node: <SwitchLabelDemo /> },
|
|
30
|
+
{ key: 'disabled', name: 'Disabled', node: <Switch value={true} onValueChange={() => {}} disabled accessibilityLabel="Notifications" /> },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { 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_SHADOW, DS_TYPOGRAPHY, DS_A11Y_MIN_TOUCH_TARGET } from '../../tokens';
|
|
4
|
+
import { useSlideAnim } from '../SegmentedToggle/useSlideAnim';
|
|
5
|
+
import { usePressScale } from '../Radio/usePressScale';
|
|
6
|
+
|
|
7
|
+
export interface SwitchProps {
|
|
8
|
+
/** Whether the switch is on. */
|
|
9
|
+
value: boolean;
|
|
10
|
+
/** Called with the new value on tap. */
|
|
11
|
+
onValueChange: (value: boolean) => void;
|
|
12
|
+
/** Optional inline label rendered to the left of the track, at the same bodyMd size/weight
|
|
13
|
+
* Checkbox/Radio use for their own labels — tapping it toggles the switch too. */
|
|
14
|
+
label?: string;
|
|
15
|
+
/** Non-interactive, dimmed. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Accessible name. Defaults to `label` when set — pass this separately only when the switch
|
|
18
|
+
* needs a different (or the only) spoken name, e.g. no visible label at all. */
|
|
19
|
+
accessibilityLabel?: string;
|
|
20
|
+
style?: StyleProp<ViewStyle>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TRACK_WIDTH = 51;
|
|
24
|
+
const TRACK_HEIGHT = 31;
|
|
25
|
+
// Sized so (TRACK_HEIGHT - THUMB_SIZE) / 2 lands on 4 — the same total inset SegmentedToggle's own
|
|
26
|
+
// thumb gets (its TRACK_PADDING + THUMB_INSET, both DS_SPACING[100]) — so the two sliding controls'
|
|
27
|
+
// thumbs read with the same amount of breathing room around them.
|
|
28
|
+
const THUMB_SIZE = 23;
|
|
29
|
+
// Horizontal inset for the thumb's off/on positions — matched to the vertical gap the track's own
|
|
30
|
+
// `justifyContent: 'center'` already gives it ((TRACK_HEIGHT - THUMB_SIZE) / 2 = 4), so the padding
|
|
31
|
+
// around the thumb reads as an even ring on every side, not tighter left/right than top/bottom.
|
|
32
|
+
const THUMB_INSET = (TRACK_HEIGHT - THUMB_SIZE) / 2;
|
|
33
|
+
// Pads the tappable area vertically out to the accessibility minimum — same reasoning as Button's
|
|
34
|
+
// own `iconOnlyHitSlop`. TRACK_WIDTH (51) already clears the minimum horizontally, so only the
|
|
35
|
+
// vertical sides need it.
|
|
36
|
+
const HIT_SLOP = {
|
|
37
|
+
top: Math.ceil((DS_A11Y_MIN_TOUCH_TARGET - TRACK_HEIGHT) / 2),
|
|
38
|
+
bottom: Math.ceil((DS_A11Y_MIN_TOUCH_TARGET - TRACK_HEIGHT) / 2),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A boolean on/off toggle. The thumb slides between its two resting positions and the track fades
|
|
43
|
+
* between its off/on colours together, driven by the same shared slide animation SegmentedToggle and
|
|
44
|
+
* UnderlineTabs use (JS-driven here, since the track colour crossfade needs it) for a consistent
|
|
45
|
+
* motion feel across the DS's sliding controls. The thumb also grows slightly while pressed, for
|
|
46
|
+
* tactile feedback before release. Kept on the same JS-driven pipeline as the slide/colour (not
|
|
47
|
+
* `useNativeDriver: true`) — mixing native- and JS-driven animations on the same Animated.View's
|
|
48
|
+
* transform is unreliable in RN, and this component's slide already has to be JS-driven anyway.
|
|
49
|
+
*/
|
|
50
|
+
export function Switch({ value, onValueChange, label, disabled = false, accessibilityLabel, style }: SwitchProps) {
|
|
51
|
+
const anim = useSlideAnim(value ? 1 : 0, false);
|
|
52
|
+
const { pressScale, onPressIn, onPressOut } = usePressScale(disabled);
|
|
53
|
+
const [focused, setFocused] = useState(false);
|
|
54
|
+
|
|
55
|
+
const trackColor = anim.interpolate({
|
|
56
|
+
inputRange: [0, 1],
|
|
57
|
+
outputRange: [DS_SEMANTIC.surface.recessed, DS_SEMANTIC.emphasis.positive],
|
|
58
|
+
});
|
|
59
|
+
const thumbX = anim.interpolate({
|
|
60
|
+
inputRange: [0, 1],
|
|
61
|
+
outputRange: [THUMB_INSET, TRACK_WIDTH - THUMB_SIZE - THUMB_INSET],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Pressable
|
|
66
|
+
onPress={() => !disabled && onValueChange(!value)}
|
|
67
|
+
onPressIn={onPressIn}
|
|
68
|
+
onPressOut={onPressOut}
|
|
69
|
+
onFocus={() => setFocused(true)}
|
|
70
|
+
onBlur={() => setFocused(false)}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
accessibilityRole="switch"
|
|
73
|
+
accessibilityState={{ checked: value, disabled }}
|
|
74
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
75
|
+
hitSlop={HIT_SLOP}
|
|
76
|
+
style={[styles.row, disabled && styles.disabled, style]}
|
|
77
|
+
>
|
|
78
|
+
{/* Label sits before the track (unlike Checkbox/Radio's control-then-label) — matches the
|
|
79
|
+
near-universal settings-row convention (description left, toggle right), while still
|
|
80
|
+
sharing their bodyMd label typography. */}
|
|
81
|
+
{!!label && <Text style={styles.label}>{label}</Text>}
|
|
82
|
+
<Animated.View style={[styles.track, { backgroundColor: trackColor }]}>
|
|
83
|
+
<Animated.View style={[styles.thumb, { transform: [{ translateX: thumbX }, { scale: pressScale }] }]} />
|
|
84
|
+
{/* Keyboard-focus ring — same interaction.focused treatment Button/Pill use; a zero-layout
|
|
85
|
+
absolute overlay so focusing never shifts layout. */}
|
|
86
|
+
{focused && !disabled && <View pointerEvents="none" style={styles.focusRing} />}
|
|
87
|
+
</Animated.View>
|
|
88
|
+
</Pressable>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const styles = StyleSheet.create({
|
|
93
|
+
row: {
|
|
94
|
+
flexDirection: 'row',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
gap: DS_SPACING[400],
|
|
97
|
+
},
|
|
98
|
+
label: {
|
|
99
|
+
...DS_TYPOGRAPHY.bodyMd,
|
|
100
|
+
color: DS_SEMANTIC.text.regular,
|
|
101
|
+
},
|
|
102
|
+
track: {
|
|
103
|
+
width: TRACK_WIDTH,
|
|
104
|
+
height: TRACK_HEIGHT,
|
|
105
|
+
borderRadius: DS_RADIUS.round,
|
|
106
|
+
justifyContent: 'center',
|
|
107
|
+
},
|
|
108
|
+
thumb: {
|
|
109
|
+
width: THUMB_SIZE,
|
|
110
|
+
height: THUMB_SIZE,
|
|
111
|
+
borderRadius: DS_RADIUS.round,
|
|
112
|
+
backgroundColor: DS_SEMANTIC.surface.white,
|
|
113
|
+
...DS_SHADOW.resting,
|
|
114
|
+
},
|
|
115
|
+
disabled: {
|
|
116
|
+
opacity: DS_SEMANTIC.interaction.disabledOpacity,
|
|
117
|
+
},
|
|
118
|
+
// 2px air + 2px ring outside the track; fully round to stay concentric.
|
|
119
|
+
focusRing: {
|
|
120
|
+
position: 'absolute',
|
|
121
|
+
top: -4,
|
|
122
|
+
bottom: -4,
|
|
123
|
+
left: -4,
|
|
124
|
+
right: -4,
|
|
125
|
+
borderWidth: 2,
|
|
126
|
+
borderColor: DS_SEMANTIC.interaction.focused,
|
|
127
|
+
borderRadius: DS_RADIUS.round,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { TextArea } from './TextArea';
|
|
4
|
+
|
|
5
|
+
function TextAreaDemo() {
|
|
6
|
+
const [value, setValue] = useState('');
|
|
7
|
+
return <TextArea value={value} onChangeText={setValue} placeholder="Describe the issue…" />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default defineCatalogPage({
|
|
11
|
+
component: 'TextArea',
|
|
12
|
+
group: 'Inputs',
|
|
13
|
+
description: 'A multi-line input that grows with its content from a minimum height.',
|
|
14
|
+
a11y: 'A multiline TextInput; pass accessibilityLabel via inputProps when there is no visible label beside it.',
|
|
15
|
+
variants: {
|
|
16
|
+
itemsFill: true,
|
|
17
|
+
items: [{ key: 'default', name: 'Default', node: <TextAreaDemo /> }],
|
|
18
|
+
},
|
|
19
|
+
states: {
|
|
20
|
+
itemsFill: true,
|
|
21
|
+
items: [
|
|
22
|
+
{
|
|
23
|
+
key: 'disabled',
|
|
24
|
+
name: 'Disabled',
|
|
25
|
+
node: <TextArea value="This report has already been submitted." onChangeText={() => {}} editable={false} />,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
TextInput,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
type StyleProp,
|
|
6
|
+
type ViewStyle,
|
|
7
|
+
type TextInputProps,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
|
|
10
|
+
import { FieldContainer } from '../FieldContainer';
|
|
11
|
+
|
|
12
|
+
export interface TextAreaProps {
|
|
13
|
+
/** Current text. */
|
|
14
|
+
value: string;
|
|
15
|
+
/** Change handler. */
|
|
16
|
+
onChangeText: (text: string) => void;
|
|
17
|
+
/** Shown when empty. */
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
/** Min height of the field before it grows with content (default 96). */
|
|
20
|
+
minHeight?: number;
|
|
21
|
+
editable?: boolean;
|
|
22
|
+
autoFocus?: boolean;
|
|
23
|
+
inputRef?: React.Ref<TextInput>;
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
/** Extra TextInput props (e.g. maxLength, accessibilityLabel, onFocus). */
|
|
26
|
+
inputProps?: Omit<
|
|
27
|
+
TextInputProps,
|
|
28
|
+
'value' | 'onChangeText' | 'placeholder' | 'editable' | 'multiline' | 'autoFocus'
|
|
29
|
+
>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A multiline free-text field — built on {@link FieldContainer} (white surface, medium radius, border
|
|
34
|
+
* that darkens on focus) with top-aligned text that grows from `minHeight`. For single-line labelled
|
|
35
|
+
* rows use {@link InputField} — both share the same FieldContainer chrome.
|
|
36
|
+
*/
|
|
37
|
+
export function TextArea({
|
|
38
|
+
value,
|
|
39
|
+
onChangeText,
|
|
40
|
+
placeholder,
|
|
41
|
+
minHeight = 96,
|
|
42
|
+
editable = true,
|
|
43
|
+
autoFocus = false,
|
|
44
|
+
inputRef,
|
|
45
|
+
style,
|
|
46
|
+
inputProps,
|
|
47
|
+
}: TextAreaProps) {
|
|
48
|
+
const [focused, setFocused] = useState(false);
|
|
49
|
+
|
|
50
|
+
const handleFocus = useCallback(
|
|
51
|
+
(e: Parameters<NonNullable<TextInputProps['onFocus']>>[0]) => {
|
|
52
|
+
setFocused(true);
|
|
53
|
+
inputProps?.onFocus?.(e);
|
|
54
|
+
},
|
|
55
|
+
[inputProps],
|
|
56
|
+
);
|
|
57
|
+
const handleBlur = useCallback(
|
|
58
|
+
(e: Parameters<NonNullable<TextInputProps['onBlur']>>[0]) => {
|
|
59
|
+
setFocused(false);
|
|
60
|
+
inputProps?.onBlur?.(e);
|
|
61
|
+
},
|
|
62
|
+
[inputProps],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<FieldContainer focused={focused} disabled={!editable} style={style}>
|
|
67
|
+
<TextInput
|
|
68
|
+
{...inputProps}
|
|
69
|
+
ref={inputRef}
|
|
70
|
+
style={[styles.input, { minHeight }, !editable && styles.inputDisabled]}
|
|
71
|
+
value={value}
|
|
72
|
+
onChangeText={onChangeText}
|
|
73
|
+
placeholder={placeholder}
|
|
74
|
+
placeholderTextColor={editable ? DS_SEMANTIC.text.muted : DS_SEMANTIC.text.disabled}
|
|
75
|
+
editable={editable}
|
|
76
|
+
autoFocus={autoFocus}
|
|
77
|
+
onFocus={handleFocus}
|
|
78
|
+
onBlur={handleBlur}
|
|
79
|
+
multiline
|
|
80
|
+
textAlignVertical="top"
|
|
81
|
+
/>
|
|
82
|
+
</FieldContainer>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const styles = StyleSheet.create({
|
|
87
|
+
// The chrome (surface/border/radius) lives on FieldContainer; the input owns the text style + padding
|
|
88
|
+
// so a tap anywhere over the padded area still focuses it.
|
|
89
|
+
input: {
|
|
90
|
+
...DS_TYPOGRAPHY.bodySm,
|
|
91
|
+
color: DS_SEMANTIC.text.regular,
|
|
92
|
+
padding: DS_SPACING[400],
|
|
93
|
+
},
|
|
94
|
+
// A step lighter than the hint tone so a non-editable area reads as inactive, not just filled —
|
|
95
|
+
// same convention as InputField/SearchField (see DS_SEMANTIC.text.disabled's own doc comment).
|
|
96
|
+
inputDisabled: {
|
|
97
|
+
color: DS_SEMANTIC.text.disabled,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { defineCatalogPage, PhoneFrame } from '@krapwoo/ds-viewer';
|
|
4
|
+
import { Button } from '../Button';
|
|
5
|
+
import { Toast } from './Toast';
|
|
6
|
+
import { DS_SPACING } from '../../tokens';
|
|
7
|
+
|
|
8
|
+
const styles = StyleSheet.create({
|
|
9
|
+
// Toast demo — floats the toast near the top of the PhoneFrame, the same way a real screen would
|
|
10
|
+
// position it (Toast itself renders no absolute overlay; that's always the call site's job).
|
|
11
|
+
toastDemoOverlay: { position: 'absolute', top: DS_SPACING[800], left: DS_SPACING[600], right: DS_SPACING[600] },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Drives Toast's `visible` prop directly (rather than the usual mount/unmount-the-whole-component
|
|
15
|
+
// pattern) so this demo actually exercises the slide-in-from-top/fade animation, not just the two
|
|
16
|
+
// static end states.
|
|
17
|
+
function ToastDemo() {
|
|
18
|
+
const [visible, setVisible] = useState(true);
|
|
19
|
+
return (
|
|
20
|
+
<PhoneFrame>
|
|
21
|
+
<Button label={visible ? 'Hide toast' : 'Show toast'} onPress={() => setVisible((v) => !v)} />
|
|
22
|
+
<View style={styles.toastDemoOverlay} pointerEvents="box-none">
|
|
23
|
+
<Toast message="Trip saved" variant="success" visible={visible} />
|
|
24
|
+
</View>
|
|
25
|
+
</PhoneFrame>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default defineCatalogPage({
|
|
30
|
+
component: 'Toast',
|
|
31
|
+
group: 'Feedback',
|
|
32
|
+
description: 'A transient confirmation bar. Without a variant it renders the dark surface with inverse text; a variant shifts to a pastel status color.',
|
|
33
|
+
whenToUse: "Transient and self-contained, floating over the screen, expected to go away on its own or via its own action. For a persistent, in-flow message about a standing condition, use Banner.",
|
|
34
|
+
a11y: 'Rendered with accessibilityRole="alert" so screen readers announce it when it appears.',
|
|
35
|
+
variants: {
|
|
36
|
+
itemsFill: true,
|
|
37
|
+
items: [
|
|
38
|
+
{ key: 'base', name: 'Base (no variant)', node: <Toast message="Changes saved" /> },
|
|
39
|
+
{ key: 'success', name: 'Success', props: { variant: 'success' }, node: <Toast message="Trip saved" variant="success" /> },
|
|
40
|
+
{ key: 'informational', name: 'Informational', props: { variant: 'informational' }, node: <Toast message="New app version available" variant="informational" /> },
|
|
41
|
+
{ key: 'warning', name: 'Warning', props: { variant: 'warning' }, node: <Toast message="Signal delays reported" variant="warning" /> },
|
|
42
|
+
{ key: 'negative', name: 'Negative', props: { variant: 'negative' }, node: <Toast message="Failed to save trip" variant="negative" /> },
|
|
43
|
+
{ key: 'neutral', name: 'Neutral', props: { variant: 'neutral' }, node: <Toast message="3 new updates" variant="neutral" /> },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
states: {
|
|
47
|
+
itemsFill: true,
|
|
48
|
+
items: [
|
|
49
|
+
{
|
|
50
|
+
key: 'action',
|
|
51
|
+
name: 'With action',
|
|
52
|
+
node: <Toast message="Trip removed" action={{ label: 'Undo', onPress: () => {} }} />,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: 'visible-toggle',
|
|
56
|
+
name: 'Slide-in animation (toggle)',
|
|
57
|
+
node: <ToastDemo />,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { View, Text, Animated, Easing, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_SPACING, DS_RADIUS, DS_TYPOGRAPHY, DS_FONT_WEIGHT, DS_SHADOW, DS_ICON_SIZE, DS_MOTION_DURATION, DS_MOTION_EASING } from '../../tokens';
|
|
4
|
+
import { Icon } from '../../icons/Icon.native';
|
|
5
|
+
import { Button } from '../Button';
|
|
6
|
+
import type { IconName } from '../../icons';
|
|
7
|
+
|
|
8
|
+
// Generously bigger than any real toast's rendered height, so the slide-in always starts fully
|
|
9
|
+
// above the visible area regardless of message length — no onLayout measurement needed (unlike
|
|
10
|
+
// Banner's collapse, this only needs "off-screen" vs "resting", not the real height).
|
|
11
|
+
const ENTER_FROM_Y = -100;
|
|
12
|
+
|
|
13
|
+
export type ToastVariant = 'success' | 'informational' | 'warning' | 'negative' | 'neutral';
|
|
14
|
+
|
|
15
|
+
// Same light tinted fill + saturated foreground pairing as Banner/Badge, so a variant toast reads as
|
|
16
|
+
// the same colour identity as the badge/banner of that status.
|
|
17
|
+
const VARIANT_CONFIG: Record<ToastVariant, { bg: string; fg: string; icon: IconName }> = {
|
|
18
|
+
success: { bg: DS_SEMANTIC.shade.positive, fg: DS_SEMANTIC.emphasis.positive, icon: 'circle-check' },
|
|
19
|
+
informational: { bg: DS_SEMANTIC.shade.info, fg: DS_SEMANTIC.emphasis.info, icon: 'info' },
|
|
20
|
+
warning: { bg: DS_SEMANTIC.shade.warning, fg: DS_SEMANTIC.emphasis.warning, icon: 'triangle-alert'},
|
|
21
|
+
negative: { bg: DS_SEMANTIC.shade.negative, fg: DS_SEMANTIC.emphasis.negative, icon: 'circle-slash' },
|
|
22
|
+
neutral: { bg: DS_SEMANTIC.shade.neutral, fg: DS_SEMANTIC.emphasis.neutral, icon: 'info-circle' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface ToastProps {
|
|
26
|
+
/** Toast text. */
|
|
27
|
+
message: string;
|
|
28
|
+
/** Whether the toast is mounted and shown — animates in by sliding down from above (+ fading in)
|
|
29
|
+
* when it becomes true, and slides back out the same way when it becomes false. @default true
|
|
30
|
+
* (renders already in its resting position, no animation — for call sites that mount/unmount the
|
|
31
|
+
* whole Toast themselves rather than toggling this prop). */
|
|
32
|
+
visible?: boolean;
|
|
33
|
+
/** When set, applies one of the semantic colour schemes (bg + default icon). */
|
|
34
|
+
variant?: ToastVariant;
|
|
35
|
+
/** Override the icon. Defaults to the variant's icon, or `circle-check` for the base style. */
|
|
36
|
+
iconName?: IconName;
|
|
37
|
+
/** Override the icon colour. Defaults to the variant's emphasis colour for variant toasts, white (matching the text) for base. */
|
|
38
|
+
iconColor?: string;
|
|
39
|
+
/** Optional trailing action — e.g. "Undo". Colour-matched to the toast's own text/icon colour
|
|
40
|
+
* (whichever that resolves to for the active variant), not a separate fixed accent. */
|
|
41
|
+
action?: { label: string; onPress: () => void };
|
|
42
|
+
style?: StyleProp<ViewStyle>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A transient confirmation bar. Without `variant`, renders the default dark surface with inverse text.
|
|
47
|
+
* With `variant`, the background shifts to the same light tinted fill Banner/Badge use for that
|
|
48
|
+
* status, with the matching saturated emphasis colour for text/icon — not white. Slides in from
|
|
49
|
+
* above (+ fades in) when `visible` turns true, and reverses on the way out — the same
|
|
50
|
+
* measure-free, JS-driven (`useNativeDriver: false`, so it's visible in the web preview too, not
|
|
51
|
+
* just on native) enter/exit pattern Dialog/BottomSheet use for their own show/hide.
|
|
52
|
+
*/
|
|
53
|
+
export function Toast({ message, visible = true, variant, iconName, iconColor, action, style }: ToastProps) {
|
|
54
|
+
const [mounted, setMounted] = useState(visible);
|
|
55
|
+
const progress = useRef(new Animated.Value(visible ? 1 : 0)).current;
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (visible) setMounted(true);
|
|
59
|
+
const duration = visible ? DS_MOTION_DURATION.base : DS_MOTION_DURATION.fast;
|
|
60
|
+
Animated.timing(progress, {
|
|
61
|
+
toValue: visible ? 1 : 0,
|
|
62
|
+
duration,
|
|
63
|
+
// `decelerate` — a toast entering the screen, per DS_MOTION_EASING_USE.
|
|
64
|
+
easing: Easing.bezier(...DS_MOTION_EASING.decelerate),
|
|
65
|
+
useNativeDriver: false,
|
|
66
|
+
}).start();
|
|
67
|
+
// A plain timer (not the animation's own `.start()` callback) drives the unmount-after-exit —
|
|
68
|
+
// `finished` can come back false (or the callback can simply not fire) when this Animated.Value
|
|
69
|
+
// is fighting for frames alongside other continuously-looping JS-driven animations elsewhere on
|
|
70
|
+
// the same screen (Loading, Shimmer), which left Toast permanently stuck invisible-but-mounted
|
|
71
|
+
// in exactly that scenario. A timer matching the animation's own duration is deterministic
|
|
72
|
+
// regardless of what the Animated internals do.
|
|
73
|
+
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
|
74
|
+
if (!visible) hideTimer = setTimeout(() => setMounted(false), duration);
|
|
75
|
+
return () => {
|
|
76
|
+
if (hideTimer != null) clearTimeout(hideTimer);
|
|
77
|
+
};
|
|
78
|
+
}, [visible, progress]);
|
|
79
|
+
|
|
80
|
+
if (!mounted) return null;
|
|
81
|
+
|
|
82
|
+
const cfg = variant ? VARIANT_CONFIG[variant] : null;
|
|
83
|
+
const resolvedIcon = iconName ?? cfg?.icon ?? 'circle-check';
|
|
84
|
+
// Message, icon (by default), and the action label all share this colour, so the whole bar reads
|
|
85
|
+
// as one consistent colour identity regardless of variant.
|
|
86
|
+
const resolvedTextColor = cfg ? cfg.fg : DS_SEMANTIC.text.inverse;
|
|
87
|
+
const resolvedIconColor = iconColor ?? resolvedTextColor;
|
|
88
|
+
|
|
89
|
+
const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [ENTER_FROM_Y, 0] });
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Animated.View
|
|
93
|
+
style={[styles.toast, cfg && { backgroundColor: cfg.bg }, { opacity: progress, transform: [{ translateY }] }, style]}
|
|
94
|
+
accessibilityRole="alert"
|
|
95
|
+
>
|
|
96
|
+
<Icon name={resolvedIcon} size={DS_ICON_SIZE.sm} color={resolvedIconColor} />
|
|
97
|
+
<Text style={[styles.text, { color: resolvedTextColor }]} numberOfLines={2}>{message}</Text>
|
|
98
|
+
{action && (
|
|
99
|
+
<>
|
|
100
|
+
{/* A plain vertical rule tinted to resolvedTextColor at reduced opacity — not the shared
|
|
101
|
+
Divider component, which is a fixed subtle-black horizontal line meant for light
|
|
102
|
+
surfaces; Toast's own background can be dark (base) or a light tint (variant), so the
|
|
103
|
+
divider has to adapt to whichever text colour is active instead. */}
|
|
104
|
+
<View style={[styles.divider, { backgroundColor: resolvedTextColor }]} />
|
|
105
|
+
{/* Always ghost + label-only — Toast's action is a quiet inline affordance, never a second
|
|
106
|
+
visual weight competing with the message. Ghost's own padding is already 0, so it sits
|
|
107
|
+
flush like the rest of the row; textStyle overrides ghost's fixed label colour with
|
|
108
|
+
resolvedTextColor so the action still colour-matches this toast's own variant. */}
|
|
109
|
+
<Button variant="ghost" size="small" label={action.label} onPress={action.onPress} textStyle={{ color: resolvedTextColor }} />
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
</Animated.View>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
toast: {
|
|
118
|
+
flexDirection: 'row',
|
|
119
|
+
alignItems: 'center',
|
|
120
|
+
gap: DS_SPACING[300],
|
|
121
|
+
// surface.inverse, not text.regular — the base toast is a dark inverse PANEL; using the ink
|
|
122
|
+
// token here would silently recolor it if a rebrand ever splits ink from dark surfaces.
|
|
123
|
+
backgroundColor: DS_SEMANTIC.surface.inverse,
|
|
124
|
+
borderRadius: DS_RADIUS.medium,
|
|
125
|
+
paddingVertical: DS_SPACING[800],
|
|
126
|
+
paddingHorizontal: DS_SPACING[600],
|
|
127
|
+
borderWidth: 2,
|
|
128
|
+
borderColor: 'transparent',
|
|
129
|
+
...DS_SHADOW.card,
|
|
130
|
+
},
|
|
131
|
+
text: {
|
|
132
|
+
flex: 1,
|
|
133
|
+
...DS_TYPOGRAPHY.bodySm,
|
|
134
|
+
fontWeight: DS_FONT_WEIGHT.medium,
|
|
135
|
+
},
|
|
136
|
+
divider: {
|
|
137
|
+
width: 1,
|
|
138
|
+
height: 20,
|
|
139
|
+
opacity: 0.3,
|
|
140
|
+
// Extra space on top of the row's own `gap` — the divider reads as a rule between two distinct
|
|
141
|
+
// regions (message vs. action), so it gets more breathing room than the icon-to-label gap does.
|
|
142
|
+
marginHorizontal: DS_SPACING[200],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
4
|
+
import { Button } from '../Button';
|
|
5
|
+
import { Tooltip } from './Tooltip';
|
|
6
|
+
|
|
7
|
+
const styles = StyleSheet.create({
|
|
8
|
+
// Tooltip demos — the bubble is an absolutely-positioned overlay above/below the trigger, sized
|
|
9
|
+
// relative to the trigger's own tiny box, not this box. A fixed-height box with the trigger
|
|
10
|
+
// centred inside gives the bubble equal clearance whichever direction it points, so stacked items
|
|
11
|
+
// (States/Configurations renders 5 of these one after another) don't crowd or overlap each other.
|
|
12
|
+
tooltipDemoBox: { height: 80, alignItems: 'center', justifyContent: 'center' },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function TooltipDemo() {
|
|
16
|
+
const [visible, setVisible] = useState(true);
|
|
17
|
+
return (
|
|
18
|
+
<Tooltip visible={visible} label="Tap to add a stop">
|
|
19
|
+
<Button
|
|
20
|
+
variant="ghost"
|
|
21
|
+
size="medium"
|
|
22
|
+
showIcon
|
|
23
|
+
showLabel={false}
|
|
24
|
+
iconName="add"
|
|
25
|
+
accessibilityLabel="Add stop"
|
|
26
|
+
onPress={() => setVisible((v) => !v)}
|
|
27
|
+
/>
|
|
28
|
+
</Tooltip>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default defineCatalogPage({
|
|
33
|
+
component: 'Tooltip',
|
|
34
|
+
group: 'Overlays',
|
|
35
|
+
description: 'A small floating label anchored above or below its wrapped trigger. Fully controlled — drive `visible` from the trigger\'s own onLongPress/onPressIn, since mobile has no hover.',
|
|
36
|
+
a11y: 'The bubble is a plain, non-interactive View; the trigger you wrap it around carries its own accessibility.',
|
|
37
|
+
variants: {
|
|
38
|
+
items: [
|
|
39
|
+
{
|
|
40
|
+
key: 'default',
|
|
41
|
+
name: 'Default (tap to toggle)',
|
|
42
|
+
props: { placement: 'top', align: 'center' },
|
|
43
|
+
node: (
|
|
44
|
+
<View style={styles.tooltipDemoBox}>
|
|
45
|
+
<TooltipDemo />
|
|
46
|
+
</View>
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
states: {
|
|
52
|
+
itemsFill: true,
|
|
53
|
+
items: [
|
|
54
|
+
{
|
|
55
|
+
key: 'bottom',
|
|
56
|
+
name: 'Placement: bottom',
|
|
57
|
+
props: { placement: 'bottom' },
|
|
58
|
+
node: (
|
|
59
|
+
<View style={styles.tooltipDemoBox}>
|
|
60
|
+
<Tooltip visible label="Tap to add a stop" placement="bottom">
|
|
61
|
+
<Button variant="ghost" size="medium" showIcon showLabel={false} iconName="add" accessibilityLabel="Add stop" onPress={() => {}} />
|
|
62
|
+
</Tooltip>
|
|
63
|
+
</View>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
// The arrow always attaches to whichever bubble edge faces the trigger — so a
|
|
68
|
+
// placement="bottom" bubble (sitting below the trigger) has its arrow on its own TOP
|
|
69
|
+
// edge, not its bottom. Naming/labels below describe the arrow's actual visual corner on
|
|
70
|
+
// the bubble, not the `placement` value, since that's what a reader looking at the
|
|
71
|
+
// rendered example actually sees.
|
|
72
|
+
key: 'top-left',
|
|
73
|
+
name: 'Top-left arrow',
|
|
74
|
+
props: { placement: 'bottom', align: 'left' },
|
|
75
|
+
node: (
|
|
76
|
+
<View style={styles.tooltipDemoBox}>
|
|
77
|
+
<Tooltip visible label="Top left" placement="bottom" align="left">
|
|
78
|
+
<Button variant="ghost" size="medium" showIcon showLabel={false} iconName="add" accessibilityLabel="Add stop" onPress={() => {}} />
|
|
79
|
+
</Tooltip>
|
|
80
|
+
</View>
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: 'top-right',
|
|
85
|
+
name: 'Top-right arrow',
|
|
86
|
+
props: { placement: 'bottom', align: 'right' },
|
|
87
|
+
node: (
|
|
88
|
+
<View style={styles.tooltipDemoBox}>
|
|
89
|
+
<Tooltip visible label="Top right" placement="bottom" align="right">
|
|
90
|
+
<Button variant="ghost" size="medium" showIcon showLabel={false} iconName="add" accessibilityLabel="Add stop" onPress={() => {}} />
|
|
91
|
+
</Tooltip>
|
|
92
|
+
</View>
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: 'bottom-left',
|
|
97
|
+
name: 'Bottom-left arrow',
|
|
98
|
+
props: { placement: 'top', align: 'left' },
|
|
99
|
+
node: (
|
|
100
|
+
<View style={styles.tooltipDemoBox}>
|
|
101
|
+
<Tooltip visible label="Bottom left" placement="top" align="left">
|
|
102
|
+
<Button variant="ghost" size="medium" showIcon showLabel={false} iconName="add" accessibilityLabel="Add stop" onPress={() => {}} />
|
|
103
|
+
</Tooltip>
|
|
104
|
+
</View>
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: 'bottom-right',
|
|
109
|
+
name: 'Bottom-right arrow',
|
|
110
|
+
props: { placement: 'top', align: 'right' },
|
|
111
|
+
node: (
|
|
112
|
+
<View style={styles.tooltipDemoBox}>
|
|
113
|
+
<Tooltip visible label="Bottom right" placement="top" align="right">
|
|
114
|
+
<Button variant="ghost" size="medium" showIcon showLabel={false} iconName="add" accessibilityLabel="Add stop" onPress={() => {}} />
|
|
115
|
+
</Tooltip>
|
|
116
|
+
</View>
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
});
|