@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,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
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { View, Text, Pressable, Modal, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { FieldContainer } from '../FieldContainer';
|
|
4
|
+
import { BottomSheet, useInsideBottomSheetWarning } from '../BottomSheet';
|
|
5
|
+
import { TopNav } from '../TopNav';
|
|
6
|
+
import { Button } from '../Button';
|
|
7
|
+
import { Icon } from '../../icons/Icon.native';
|
|
8
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY, DS_ICON_SIZE, DS_MOTION_DURATION, DS_A11Y_MIN_TOUCH_TARGET } from '../../tokens';
|
|
9
|
+
|
|
10
|
+
export interface DropdownOption {
|
|
11
|
+
value: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DropdownProps {
|
|
16
|
+
/** Field label shown above the value. Generic text (unlike InputField's fixed label set) since a
|
|
17
|
+
* dropdown's label is app-specific. */
|
|
18
|
+
label?: string;
|
|
19
|
+
/** Selected option's value. */
|
|
20
|
+
value?: string;
|
|
21
|
+
/** Shown when no option is selected. */
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
/** The selectable options. */
|
|
24
|
+
options: DropdownOption[];
|
|
25
|
+
/** Called with the newly selected option's value. */
|
|
26
|
+
onChange: (value: string) => void;
|
|
27
|
+
/** Non-interactive, dimmed; the picker won't open. */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
style?: StyleProp<ViewStyle>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A labelled field (on the shared {@link FieldContainer} chrome) that opens a {@link BottomSheet}
|
|
34
|
+
* picker on tap — the "bottom sheet picker" interaction: tap the trigger, pick an option from the
|
|
35
|
+
* sheet, it closes. Value/selection are controlled by the caller; the sheet's own open/closed state
|
|
36
|
+
* is internal.
|
|
37
|
+
*/
|
|
38
|
+
export function Dropdown({ label, value, placeholder, options, onChange, disabled = false, style }: DropdownProps) {
|
|
39
|
+
useInsideBottomSheetWarning('Dropdown');
|
|
40
|
+
const [open, setOpen] = useState(false);
|
|
41
|
+
// The picker is portaled through a Modal (see the render below) specifically so it covers the
|
|
42
|
+
// whole screen even when Dropdown itself sits deep inside an arbitrary layout (a form row, a
|
|
43
|
+
// card, …) — BottomSheet's own absolutely-positioned overlay only fills whichever immediate
|
|
44
|
+
// parent View hosts it, which is wrong the moment that parent isn't already the full screen.
|
|
45
|
+
// `sheetMounted` keeps the Modal (and thus BottomSheet) around for BottomSheet's own exit
|
|
46
|
+
// animation instead of yanking it out of the DOM the instant `open` goes false.
|
|
47
|
+
const [sheetMounted, setSheetMounted] = useState(false);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (open) {
|
|
50
|
+
setSheetMounted(true);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const timer = setTimeout(() => setSheetMounted(false), DS_MOTION_DURATION.fast);
|
|
54
|
+
return () => clearTimeout(timer);
|
|
55
|
+
}, [open]);
|
|
56
|
+
const selected = options.find((o) => o.value === value);
|
|
57
|
+
const hasValue = !!selected;
|
|
58
|
+
|
|
59
|
+
const handleSelect = (optionValue: string) => {
|
|
60
|
+
onChange(optionValue);
|
|
61
|
+
setOpen(false);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const triggerAccessibilityLabel = [label, hasValue ? selected.label : placeholder].filter(Boolean).join(', ');
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<FieldContainer
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
onPress={disabled ? undefined : () => setOpen(true)}
|
|
71
|
+
// Label + state stay on even when disabled — FieldContainer keeps its View branch accessible,
|
|
72
|
+
// so a disabled dropdown still announces as "…, button, disabled" instead of vanishing.
|
|
73
|
+
accessibilityLabel={triggerAccessibilityLabel}
|
|
74
|
+
accessibilityState={{ disabled, expanded: open }}
|
|
75
|
+
style={[styles.trigger, style]}
|
|
76
|
+
>
|
|
77
|
+
<View style={styles.inner}>
|
|
78
|
+
<View style={styles.textColumn}>
|
|
79
|
+
{!!label && <Text style={[styles.label, disabled && styles.disabledText]}>{label}</Text>}
|
|
80
|
+
{/* Hint (empty) and disabled are DIFFERENT states with different colors — hint keeps the
|
|
81
|
+
text.muted placeholder tone; disabled goes a step lighter (text.disabled) so a
|
|
82
|
+
disabled dropdown holding a real value never reads as merely empty. */}
|
|
83
|
+
<Text
|
|
84
|
+
style={[styles.value, !hasValue && !disabled && styles.hint, disabled && styles.disabledText]}
|
|
85
|
+
numberOfLines={1}
|
|
86
|
+
>
|
|
87
|
+
{hasValue ? selected.label : placeholder}
|
|
88
|
+
</Text>
|
|
89
|
+
</View>
|
|
90
|
+
{/* Centred against the whole field (label + value stacked), not just the value line — this
|
|
91
|
+
row spans the full trigger height via `inner`'s own alignItems:'center'. */}
|
|
92
|
+
<Icon name="chevron-down" size={DS_ICON_SIZE.sm} color={disabled ? DS_SEMANTIC.text.disabled : DS_SEMANTIC.text.regular} />
|
|
93
|
+
</View>
|
|
94
|
+
</FieldContainer>
|
|
95
|
+
{sheetMounted && (
|
|
96
|
+
<Modal transparent animationType="none" onRequestClose={() => setOpen(false)}>
|
|
97
|
+
<BottomSheet
|
|
98
|
+
visible={open}
|
|
99
|
+
onDismiss={() => setOpen(false)}
|
|
100
|
+
header={
|
|
101
|
+
<TopNav
|
|
102
|
+
title={label ?? 'Select an option'}
|
|
103
|
+
trailing={
|
|
104
|
+
<Button variant="secondary" size="small" showIcon showLabel={false} iconName="clear" accessibilityLabel="Close" onPress={() => setOpen(false)} />
|
|
105
|
+
}
|
|
106
|
+
/>
|
|
107
|
+
}
|
|
108
|
+
>
|
|
109
|
+
<View style={styles.options} accessibilityRole="radiogroup">
|
|
110
|
+
{options.map((option) => {
|
|
111
|
+
const isSelected = option.value === value;
|
|
112
|
+
return (
|
|
113
|
+
<Pressable
|
|
114
|
+
key={option.value}
|
|
115
|
+
onPress={() => handleSelect(option.value)}
|
|
116
|
+
accessibilityRole="radio"
|
|
117
|
+
accessibilityState={{ checked: isSelected }}
|
|
118
|
+
style={({ pressed }) => [styles.option, pressed && styles.optionPressed]}
|
|
119
|
+
>
|
|
120
|
+
<Text style={[styles.optionLabel, isSelected && styles.optionLabelSelected]}>{option.label}</Text>
|
|
121
|
+
{isSelected && <Icon name="check" size={DS_ICON_SIZE.sm} color={DS_SEMANTIC.emphasis.info} />}
|
|
122
|
+
</Pressable>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</View>
|
|
126
|
+
</BottomSheet>
|
|
127
|
+
</Modal>
|
|
128
|
+
)}
|
|
129
|
+
</>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const styles = StyleSheet.create({
|
|
134
|
+
trigger: {
|
|
135
|
+
width: '100%',
|
|
136
|
+
paddingHorizontal: DS_SPACING[800],
|
|
137
|
+
paddingVertical: DS_SPACING[400],
|
|
138
|
+
// A labelled trigger is ~56pt already; this floor covers the label-less case, which would
|
|
139
|
+
// otherwise land under the 44pt minimum touch target.
|
|
140
|
+
minHeight: DS_A11Y_MIN_TOUCH_TARGET,
|
|
141
|
+
justifyContent: 'center',
|
|
142
|
+
},
|
|
143
|
+
inner: { flexDirection: 'row', alignItems: 'center', gap: DS_SPACING[400] },
|
|
144
|
+
textColumn: { flex: 1, gap: DS_SPACING[100] },
|
|
145
|
+
label: { ...DS_TYPOGRAPHY.labelXs, color: DS_SEMANTIC.text.muted },
|
|
146
|
+
value: { ...DS_TYPOGRAPHY.bodyMd, color: DS_SEMANTIC.text.regular },
|
|
147
|
+
// Placeholder tone for the empty state.
|
|
148
|
+
hint: { color: DS_SEMANTIC.text.muted },
|
|
149
|
+
// Disabled text — one step lighter than `hint` (see DS_SEMANTIC.text.disabled's doc comment).
|
|
150
|
+
disabledText: { color: DS_SEMANTIC.text.disabled },
|
|
151
|
+
// No padding here — BottomSheet's own content area now supplies the standard 16px on all sides.
|
|
152
|
+
options: {},
|
|
153
|
+
option: {
|
|
154
|
+
flexDirection: 'row',
|
|
155
|
+
alignItems: 'center',
|
|
156
|
+
justifyContent: 'space-between',
|
|
157
|
+
paddingVertical: DS_SPACING[600],
|
|
158
|
+
},
|
|
159
|
+
optionPressed: { backgroundColor: DS_SEMANTIC.interaction.pressed },
|
|
160
|
+
optionLabel: { ...DS_TYPOGRAPHY.bodyMd, color: DS_SEMANTIC.text.regular },
|
|
161
|
+
optionLabelSelected: { color: DS_SEMANTIC.emphasis.info },
|
|
162
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { EmptyState } from './EmptyState';
|
|
4
|
+
import { DS_SEMANTIC } from '../../tokens';
|
|
5
|
+
|
|
6
|
+
export default defineCatalogPage({
|
|
7
|
+
component: 'EmptyState',
|
|
8
|
+
group: 'Layout',
|
|
9
|
+
description: 'A centred placeholder for a screen or section with nothing to show yet — no results, no saved items, a first-run state.',
|
|
10
|
+
whenToUse: "Fills the entire content area because there's nothing else to show. For a note that sits alongside other real content, use Banner instead.",
|
|
11
|
+
a11y: 'A plain View with text; the action(s) render as real Buttons inside a ButtonGroup, which carry their own accessibility.',
|
|
12
|
+
// No `variant` prop exists on EmptyState — its "Variants" column shows the minimal default
|
|
13
|
+
// (iconName omitted, so it falls back to the real default) — the description/action
|
|
14
|
+
// configurations live under "States".
|
|
15
|
+
variants: {
|
|
16
|
+
itemsFill: true,
|
|
17
|
+
items: [{ key: 'default', name: 'Default', node: <EmptyState title="No saved trips yet" /> }],
|
|
18
|
+
},
|
|
19
|
+
states: {
|
|
20
|
+
itemsFill: true,
|
|
21
|
+
items: [
|
|
22
|
+
{
|
|
23
|
+
key: 'description',
|
|
24
|
+
name: 'With description',
|
|
25
|
+
node: <EmptyState iconName="search" title="No results found" description="Try a different station or address." />,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: 'action',
|
|
29
|
+
name: 'With action',
|
|
30
|
+
node: (
|
|
31
|
+
<EmptyState
|
|
32
|
+
iconName="waypoints"
|
|
33
|
+
title="No saved trips yet"
|
|
34
|
+
description="Your saved trips will show up here."
|
|
35
|
+
action={{ label: 'Start a trip', onPress: () => {} }}
|
|
36
|
+
/>
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: 'secondary-action',
|
|
41
|
+
name: 'With two actions',
|
|
42
|
+
props: { secondaryAction: true },
|
|
43
|
+
node: (
|
|
44
|
+
<EmptyState
|
|
45
|
+
iconName="waypoints"
|
|
46
|
+
title="No saved trips yet"
|
|
47
|
+
description="Your saved trips will show up here."
|
|
48
|
+
action={{ label: 'Start a trip', onPress: () => {} }}
|
|
49
|
+
secondaryAction={{ label: 'Not now', onPress: () => {} }}
|
|
50
|
+
/>
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'custom-color',
|
|
55
|
+
name: 'Custom avatar colour',
|
|
56
|
+
node: (
|
|
57
|
+
<EmptyState
|
|
58
|
+
iconName="triangle-alert"
|
|
59
|
+
title="Something went wrong"
|
|
60
|
+
description="Check your connection and try again."
|
|
61
|
+
backgroundColor={DS_SEMANTIC.emphasis.negative}
|
|
62
|
+
/>
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { Avatar } from '../Avatar';
|
|
4
|
+
import { Button } from '../Button';
|
|
5
|
+
import { ButtonGroup } from '../ButtonGroup';
|
|
6
|
+
import type { IconName } from '../../icons';
|
|
7
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
|
|
8
|
+
|
|
9
|
+
const AVATAR_SIZE = 64;
|
|
10
|
+
|
|
11
|
+
export interface EmptyStateAction {
|
|
12
|
+
label: string;
|
|
13
|
+
onPress: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EmptyStateProps {
|
|
17
|
+
/** Icon shown above the title, on an Avatar circle. Defaults to `users` — the same generic-person
|
|
18
|
+
* glyph Avatar's own icon fallback uses, a reasonable default when nothing more specific fits
|
|
19
|
+
* ("no saved trips", "no results"). Pass a more specific icon when one obviously fits. */
|
|
20
|
+
iconName?: IconName;
|
|
21
|
+
/** Override the avatar circle's fill — same prop, same meaning as Avatar's own `backgroundColor`.
|
|
22
|
+
* Defaults to Avatar's own default (a neutral muted surface). */
|
|
23
|
+
backgroundColor?: string;
|
|
24
|
+
/** Primary message. */
|
|
25
|
+
title: string;
|
|
26
|
+
/** Supporting line below the title — what's empty, or what to do about it. */
|
|
27
|
+
description?: string;
|
|
28
|
+
/** Primary action rendered below the description (e.g. "Start a trip"). */
|
|
29
|
+
action?: EmptyStateAction;
|
|
30
|
+
/** Optional second action, stacked below `action` as a tertiary button (e.g. "Not now"). Only
|
|
31
|
+
* rendered when `action` is also set — a secondary action with no primary one doesn't make sense. */
|
|
32
|
+
secondaryAction?: EmptyStateAction;
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A centred placeholder for a screen or section with nothing to show yet — no results, no saved
|
|
38
|
+
* items, a first-run state. An Avatar (icon on a circular fill, not a bare icon — reuses the same
|
|
39
|
+
* component and default styling a real profile picture would use here), title, optional
|
|
40
|
+
* description, optional action(s), always in that order and always centred; for anything more
|
|
41
|
+
* custom (illustrations, more than two actions), compose your own layout instead of extending this
|
|
42
|
+
* one. `action`/`secondaryAction` render through `ButtonGroup`'s `vertical` layout — always `small`
|
|
43
|
+
* size (EmptyState's own actions read as a lighter-weight, secondary moment, never the primary CTA
|
|
44
|
+
* of the screen they sit in) — so the primary/tertiary pair already follows ButtonGroup's own
|
|
45
|
+
* composition rules (matching size, matching label-only configuration) with nothing extra to wire up.
|
|
46
|
+
*/
|
|
47
|
+
export function EmptyState({ iconName = 'users', backgroundColor, title, description, action, secondaryAction, style }: EmptyStateProps) {
|
|
48
|
+
return (
|
|
49
|
+
<View style={[styles.container, style]}>
|
|
50
|
+
<Avatar iconName={iconName} size={AVATAR_SIZE} backgroundColor={backgroundColor} />
|
|
51
|
+
<Text style={styles.title}>{title}</Text>
|
|
52
|
+
{!!description && <Text style={styles.description}>{description}</Text>}
|
|
53
|
+
{action && (
|
|
54
|
+
<ButtonGroup variant="vertical" style={styles.actionWrap}>
|
|
55
|
+
<Button label={action.label} onPress={action.onPress} size="small" />
|
|
56
|
+
{secondaryAction && <Button variant="tertiary" label={secondaryAction.label} onPress={secondaryAction.onPress} size="small" />}
|
|
57
|
+
</ButtonGroup>
|
|
58
|
+
)}
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const styles = StyleSheet.create({
|
|
64
|
+
container: {
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
paddingHorizontal: DS_SPACING[1200],
|
|
67
|
+
paddingVertical: DS_SPACING[2000],
|
|
68
|
+
gap: DS_SPACING[400],
|
|
69
|
+
},
|
|
70
|
+
title: {
|
|
71
|
+
...DS_TYPOGRAPHY.labelMd,
|
|
72
|
+
color: DS_SEMANTIC.text.regular,
|
|
73
|
+
textAlign: 'center',
|
|
74
|
+
marginTop: DS_SPACING[400],
|
|
75
|
+
},
|
|
76
|
+
description: {
|
|
77
|
+
...DS_TYPOGRAPHY.bodySm,
|
|
78
|
+
color: DS_SEMANTIC.text.muted,
|
|
79
|
+
textAlign: 'center',
|
|
80
|
+
},
|
|
81
|
+
actionWrap: {
|
|
82
|
+
marginTop: DS_SPACING[400],
|
|
83
|
+
// Overrides ButtonGroup vertical's own default full-width stretch (right for Dock's button bar,
|
|
84
|
+
// wrong here) — EmptyState's actions read as a secondary, content-sized moment, not a bar
|
|
85
|
+
// spanning the whole width.
|
|
86
|
+
alignItems: 'center',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
4
|
+
import { FieldContainer } from './FieldContainer';
|
|
5
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY } from '../../tokens';
|
|
6
|
+
|
|
7
|
+
const styles = StyleSheet.create({
|
|
8
|
+
// FieldContainer — a stand-in "field" body, since the real component only supplies the chrome.
|
|
9
|
+
fieldContainerDemo: { height: 56, justifyContent: 'center', paddingHorizontal: DS_SPACING[800] },
|
|
10
|
+
fieldContainerText: { ...DS_TYPOGRAPHY.bodyMd, color: DS_SEMANTIC.text.muted },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export default defineCatalogPage({
|
|
14
|
+
component: 'FieldContainer',
|
|
15
|
+
group: 'Sub-Parts',
|
|
16
|
+
description: 'The shared field chrome — white surface, medium radius, a 1px border that darkens on focus and mutes when disabled — behind InputField, TextArea, Dropdown, and SearchField, so all four share one visually-consistent field look instead of each re-implementing it.',
|
|
17
|
+
a11y: 'Renders a plain View by default. Passing onPress (without disabled) makes it a Pressable with accessibilityRole="button" and the given accessibilityLabel — a consumer building a real editable field (InputField\'s editable mode) relies on its own inner TextInput for accessibility instead.',
|
|
18
|
+
variants: {
|
|
19
|
+
itemsFill: true,
|
|
20
|
+
items: [
|
|
21
|
+
{
|
|
22
|
+
key: 'resting',
|
|
23
|
+
name: 'Resting',
|
|
24
|
+
node: (
|
|
25
|
+
<FieldContainer style={styles.fieldContainerDemo}>
|
|
26
|
+
<Text style={styles.fieldContainerText}>Field content</Text>
|
|
27
|
+
</FieldContainer>
|
|
28
|
+
),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: 'focused',
|
|
32
|
+
name: 'Focused',
|
|
33
|
+
props: { focused: true },
|
|
34
|
+
node: (
|
|
35
|
+
<FieldContainer focused style={styles.fieldContainerDemo}>
|
|
36
|
+
<Text style={styles.fieldContainerText}>Field content</Text>
|
|
37
|
+
</FieldContainer>
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'disabled',
|
|
42
|
+
name: 'Disabled',
|
|
43
|
+
props: { disabled: true },
|
|
44
|
+
node: (
|
|
45
|
+
<FieldContainer disabled style={styles.fieldContainerDemo}>
|
|
46
|
+
<Text style={styles.fieldContainerText}>Field content</Text>
|
|
47
|
+
</FieldContainer>
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
states: {
|
|
53
|
+
itemsFill: true,
|
|
54
|
+
items: [
|
|
55
|
+
{
|
|
56
|
+
key: 'pressed',
|
|
57
|
+
name: 'Pressed (caller-driven)',
|
|
58
|
+
props: { pressed: true },
|
|
59
|
+
node: (
|
|
60
|
+
<FieldContainer pressed style={styles.fieldContainerDemo}>
|
|
61
|
+
<Text style={styles.fieldContainerText}>Field content</Text>
|
|
62
|
+
</FieldContainer>
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'pressable',
|
|
67
|
+
name: 'Pressable (with onPress)',
|
|
68
|
+
node: (
|
|
69
|
+
<FieldContainer onPress={() => {}} accessibilityLabel="Field content" style={styles.fieldContainerDemo}>
|
|
70
|
+
<Text style={styles.fieldContainerText}>Field content</Text>
|
|
71
|
+
</FieldContainer>
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Pressable, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_RADIUS } from '../../tokens';
|
|
4
|
+
import { useSurfaceTone } from '../Surface';
|
|
5
|
+
import type { FieldContainerProps } from './FieldContainer.types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The shared "field" chrome: medium radius, a 1px subtle border that darkens to border.dark on
|
|
9
|
+
* focus, and a fill that contrasts with whatever it's actually placed on — reads the ambient
|
|
10
|
+
* `useSurfaceTone()` (see `Surface`) rather than assuming one fixed background. On the default
|
|
11
|
+
* white tone it's a translucent recessed fill (the same adaptive-overlay mechanism SegmentedToggle's
|
|
12
|
+
* track and Button's own `secondary` variant use); on a `muted` surface (wrapped in
|
|
13
|
+
* `<Surface tone="muted">`) it flips to opaque white instead, so it never goes invisible against
|
|
14
|
+
* whatever's behind it either way. It owns *only* the container look — consumers (InputField,
|
|
15
|
+
* TextArea, SearchField) supply their own layout (height, padding, content) via `style` and
|
|
16
|
+
* children, so a single-line row and a multiline area stay visually consistent.
|
|
17
|
+
*/
|
|
18
|
+
export function FieldContainer({
|
|
19
|
+
focused = false,
|
|
20
|
+
disabled = false,
|
|
21
|
+
onPress,
|
|
22
|
+
pressed = false,
|
|
23
|
+
accessibilityLabel,
|
|
24
|
+
accessibilityState,
|
|
25
|
+
style,
|
|
26
|
+
children,
|
|
27
|
+
}: FieldContainerProps) {
|
|
28
|
+
const onWhite = useSurfaceTone() === 'white';
|
|
29
|
+
const toneStyle = onWhite ? stylesOnWhite : stylesOnMuted;
|
|
30
|
+
|
|
31
|
+
const stylesFor = (isPressed: boolean) => [
|
|
32
|
+
styles.base,
|
|
33
|
+
toneStyle.rest,
|
|
34
|
+
disabled && toneStyle.disabled,
|
|
35
|
+
!disabled && focused && toneStyle.focused,
|
|
36
|
+
!disabled && isPressed && toneStyle.pressed,
|
|
37
|
+
style,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
if (onPress && !disabled) {
|
|
41
|
+
// Pressable owns the pressed feedback; OR in the caller-driven `pressed` so both sources work.
|
|
42
|
+
return (
|
|
43
|
+
<Pressable
|
|
44
|
+
onPress={onPress}
|
|
45
|
+
accessibilityRole="button"
|
|
46
|
+
accessibilityLabel={accessibilityLabel}
|
|
47
|
+
accessibilityState={accessibilityState}
|
|
48
|
+
style={({ pressed: p }) => stylesFor(p || pressed)}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</Pressable>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Non-Pressable (e.g. editable, or a disabled picker) fields still honour the caller-driven
|
|
56
|
+
// pressed treatment. When the caller gave this container a name/state, keep it an accessible
|
|
57
|
+
// button element — a disabled picker must announce as "…, button, disabled", not vanish from
|
|
58
|
+
// the accessibility tree just because it lost its Pressable.
|
|
59
|
+
const stillAccessible = accessibilityLabel != null || accessibilityState != null;
|
|
60
|
+
return (
|
|
61
|
+
<View
|
|
62
|
+
style={stylesFor(pressed)}
|
|
63
|
+
accessible={stillAccessible || undefined}
|
|
64
|
+
accessibilityRole={stillAccessible ? 'button' : undefined}
|
|
65
|
+
accessibilityLabel={accessibilityLabel}
|
|
66
|
+
accessibilityState={accessibilityState}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const styles = StyleSheet.create({
|
|
74
|
+
base: {
|
|
75
|
+
borderRadius: DS_RADIUS.medium,
|
|
76
|
+
borderWidth: 1,
|
|
77
|
+
borderColor: DS_SEMANTIC.border.subtle,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// On a white surface: translucent recessed fill, tuned pressed/disabled versions of that same
|
|
82
|
+
// overlay (recessedPressed/recessedDisabled — not a second overlay stacked on top, see Button's own
|
|
83
|
+
// `secondary` variant for why).
|
|
84
|
+
const stylesOnWhite = StyleSheet.create({
|
|
85
|
+
rest: { backgroundColor: DS_SEMANTIC.surface.recessed },
|
|
86
|
+
focused: { borderColor: DS_SEMANTIC.border.dark },
|
|
87
|
+
pressed: { backgroundColor: DS_SEMANTIC.surface.recessedPressed },
|
|
88
|
+
disabled: { backgroundColor: DS_SEMANTIC.surface.recessedDisabled, borderColor: DS_SEMANTIC.border.subtle },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// On a muted surface: opaque white fill instead (recessed would barely read against an
|
|
92
|
+
// already-grey background) — pressed/disabled are the plain surface tokens for "a white
|
|
93
|
+
// surface, tapped" / "a white surface, muted-out", the same ones other white-fill surfaces use.
|
|
94
|
+
const stylesOnMuted = StyleSheet.create({
|
|
95
|
+
rest: { backgroundColor: DS_SEMANTIC.surface.white },
|
|
96
|
+
focused: { borderColor: DS_SEMANTIC.border.dark },
|
|
97
|
+
pressed: { backgroundColor: DS_SEMANTIC.surface.onTap },
|
|
98
|
+
disabled: { backgroundColor: DS_SEMANTIC.surface.muted, borderColor: DS_SEMANTIC.border.subtle },
|
|
99
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { AccessibilityState, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface FieldContainerProps {
|
|
5
|
+
/** Focused → border darkens to border.dark. */
|
|
6
|
+
focused?: boolean;
|
|
7
|
+
/** Disabled → washed-out fill, non-interactive. The exact fill adapts to the ambient surface tone
|
|
8
|
+
* (see FieldContainer.tsx's per-tone StyleSheets). */
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** When set (and not disabled), the container is a Pressable with tap feedback — the pressed fill
|
|
11
|
+
* adapts to the ambient surface tone (see FieldContainer.tsx). */
|
|
12
|
+
onPress?: () => void;
|
|
13
|
+
/** Caller-driven pressed state — applies the same pressed treatment without an `onPress` Pressable
|
|
14
|
+
* (e.g. an editable field that drives this from the TextInput's onPressIn/onPressOut). */
|
|
15
|
+
pressed?: boolean;
|
|
16
|
+
/** Only used when onPress is set. */
|
|
17
|
+
accessibilityLabel?: string;
|
|
18
|
+
/** Forwarded to the container (Pressable or plain View) — lets a picker-style consumer announce
|
|
19
|
+
* `{ disabled, expanded }`. When set (or when `accessibilityLabel` is), even the non-Pressable
|
|
20
|
+
* branch stays an accessible `button` element, so e.g. a disabled Dropdown still announces as
|
|
21
|
+
* "…, button, disabled" instead of disappearing from the accessibility tree. */
|
|
22
|
+
accessibilityState?: AccessibilityState;
|
|
23
|
+
/** Layout the consumer adds on top of the shared chrome (height, padding, flex direction…). */
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
/** The field's own layout, height, padding, and content. */
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { InputClearButton } from './InputClearButton';
|
|
4
|
+
|
|
5
|
+
export default defineCatalogPage({
|
|
6
|
+
component: 'InputClearButton',
|
|
7
|
+
group: 'Sub-Parts',
|
|
8
|
+
description: 'The clear (×) button InputField and SearchField both show once a field is active and holds a value — a filled circle-x icon sized to reach the 44pt touch target via hitSlop, not visual size.',
|
|
9
|
+
a11y: 'A Pressable with accessibilityRole="button" and the given (or default "Clear field") accessibilityLabel; hitSlop of 10 on every side pads its 24×24 visual size out to the 44pt minimum.',
|
|
10
|
+
variants: {
|
|
11
|
+
items: [{ key: 'default', name: 'Default', node: <InputClearButton onPress={() => {}} /> }],
|
|
12
|
+
},
|
|
13
|
+
states: {
|
|
14
|
+
items: [
|
|
15
|
+
{ key: 'custom-label', name: 'Custom accessibility label', node: <InputClearButton onPress={() => {}} accessibilityLabel="Clear search" /> },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
});
|