@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,165 @@
|
|
|
1
|
+
import React, { useState, type ReactNode } from 'react';
|
|
2
|
+
import { View, Text, Pressable, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_SPACING, DS_TYPOGRAPHY, DS_A11Y_MIN_TOUCH_TARGET } from '../../tokens';
|
|
4
|
+
|
|
5
|
+
export interface ListItemProps {
|
|
6
|
+
/** Primary text. */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Small secondary line below the title. */
|
|
9
|
+
subtitle?: string;
|
|
10
|
+
/** A third block below subtitle — for lower-priority extra content (a timestamp, a count, or a
|
|
11
|
+
* Badge/Button) that shouldn't compete with subtitle for attention. A plain string renders in the
|
|
12
|
+
* same small muted style as before; pass a Badge/Button/other node directly for anything richer. */
|
|
13
|
+
footer?: ReactNode;
|
|
14
|
+
/** Leading slot — typically an Avatar or Icon. */
|
|
15
|
+
leading?: ReactNode;
|
|
16
|
+
/** Compact right-aligned value shown before the trailing slot — e.g. a settings row's current
|
|
17
|
+
* value ("English") ahead of its chevron. */
|
|
18
|
+
trailingText?: string;
|
|
19
|
+
/** A second, more muted right-aligned line below `trailingText` — e.g. a unit or a status caption
|
|
20
|
+
* under the primary value. Only meaningful alongside `trailingText`. */
|
|
21
|
+
trailingSubtext?: string;
|
|
22
|
+
/** Trailing slot — typically a chevron Icon, a Switch, a Button, or a value label. */
|
|
23
|
+
trailing?: ReactNode;
|
|
24
|
+
/** Makes the whole row tappable. */
|
|
25
|
+
onPress?: () => void;
|
|
26
|
+
/** Non-interactive, dimmed. */
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
style?: StyleProp<ViewStyle>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A single row: optional leading/trailing slots flanking a title (+ optional subtitle/footer).
|
|
32
|
+
* Stack several inside a {@link List} for a settings screen, menu, or search-results list. */
|
|
33
|
+
export function ListItem({
|
|
34
|
+
title,
|
|
35
|
+
subtitle,
|
|
36
|
+
footer,
|
|
37
|
+
leading,
|
|
38
|
+
trailingText,
|
|
39
|
+
trailingSubtext,
|
|
40
|
+
trailing,
|
|
41
|
+
onPress,
|
|
42
|
+
disabled = false,
|
|
43
|
+
style,
|
|
44
|
+
}: ListItemProps) {
|
|
45
|
+
const [focused, setFocused] = useState(false);
|
|
46
|
+
const inner = (
|
|
47
|
+
<>
|
|
48
|
+
{leading && <View style={styles.slot}>{leading}</View>}
|
|
49
|
+
<View style={styles.textGroup}>
|
|
50
|
+
<Text style={[styles.title, disabled && styles.disabledText]} numberOfLines={1}>{title}</Text>
|
|
51
|
+
{!!subtitle && (
|
|
52
|
+
<Text style={[styles.subtitle, disabled && styles.disabledText]} numberOfLines={1}>{subtitle}</Text>
|
|
53
|
+
)}
|
|
54
|
+
{footer != null &&
|
|
55
|
+
(typeof footer === 'string' ? (
|
|
56
|
+
<Text style={[styles.footerText, disabled && styles.disabledText]} numberOfLines={1}>{footer}</Text>
|
|
57
|
+
) : (
|
|
58
|
+
<View style={styles.footerNode}>{footer}</View>
|
|
59
|
+
))}
|
|
60
|
+
</View>
|
|
61
|
+
{(trailingText || trailing) && (
|
|
62
|
+
<View style={styles.trailingGroup}>
|
|
63
|
+
{!!trailingText && (
|
|
64
|
+
<View style={styles.trailingTextGroup}>
|
|
65
|
+
<Text style={[styles.trailingText, disabled && styles.disabledText]} numberOfLines={1}>{trailingText}</Text>
|
|
66
|
+
{!!trailingSubtext && (
|
|
67
|
+
<Text style={[styles.trailingSubtext, disabled && styles.disabledText]} numberOfLines={1}>{trailingSubtext}</Text>
|
|
68
|
+
)}
|
|
69
|
+
</View>
|
|
70
|
+
)}
|
|
71
|
+
{trailing && <View style={styles.slot}>{trailing}</View>}
|
|
72
|
+
</View>
|
|
73
|
+
)}
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (onPress) {
|
|
78
|
+
return (
|
|
79
|
+
<Pressable
|
|
80
|
+
onPress={disabled ? undefined : onPress}
|
|
81
|
+
onFocus={() => setFocused(true)}
|
|
82
|
+
onBlur={() => setFocused(false)}
|
|
83
|
+
disabled={disabled}
|
|
84
|
+
accessibilityRole="button"
|
|
85
|
+
accessibilityLabel={[title, subtitle].filter(Boolean).join(', ')}
|
|
86
|
+
accessibilityState={{ disabled }}
|
|
87
|
+
// Keyboard focus shows the same highlight as a press — a full-width row has no border to
|
|
88
|
+
// recolor, so focus mirrors the pressed treatment (same policy as SegmentedToggle/UnderlineTabs).
|
|
89
|
+
style={({ pressed }) => [styles.row, (pressed || focused) && !disabled && styles.pressed, style]}
|
|
90
|
+
>
|
|
91
|
+
{inner}
|
|
92
|
+
</Pressable>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return <View style={[styles.row, style]}>{inner}</View>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const styles = StyleSheet.create({
|
|
100
|
+
row: {
|
|
101
|
+
width: '100%',
|
|
102
|
+
flexDirection: 'row',
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
gap: DS_SPACING[600],
|
|
105
|
+
minHeight: DS_A11Y_MIN_TOUCH_TARGET,
|
|
106
|
+
paddingVertical: DS_SPACING[800],
|
|
107
|
+
paddingHorizontal: DS_SPACING[400],
|
|
108
|
+
backgroundColor: DS_SEMANTIC.surface.white,
|
|
109
|
+
},
|
|
110
|
+
pressed: {
|
|
111
|
+
backgroundColor: DS_SEMANTIC.interaction.pressed,
|
|
112
|
+
},
|
|
113
|
+
slot: {
|
|
114
|
+
flexShrink: 0,
|
|
115
|
+
},
|
|
116
|
+
textGroup: {
|
|
117
|
+
flex: 1,
|
|
118
|
+
minWidth: 0,
|
|
119
|
+
gap: DS_SPACING[100],
|
|
120
|
+
},
|
|
121
|
+
title: {
|
|
122
|
+
...DS_TYPOGRAPHY.bodyMd,
|
|
123
|
+
color: DS_SEMANTIC.text.regular,
|
|
124
|
+
},
|
|
125
|
+
subtitle: {
|
|
126
|
+
...DS_TYPOGRAPHY.bodyXs,
|
|
127
|
+
color: DS_SEMANTIC.text.muted,
|
|
128
|
+
},
|
|
129
|
+
footerText: {
|
|
130
|
+
...DS_TYPOGRAPHY.labelXs,
|
|
131
|
+
color: DS_SEMANTIC.text.muted,
|
|
132
|
+
},
|
|
133
|
+
// A node footer (Badge/Button) brings its own sizing/color — just a top nudge so it doesn't sit
|
|
134
|
+
// flush against subtitle.
|
|
135
|
+
footerNode: {
|
|
136
|
+
marginTop: DS_SPACING[100],
|
|
137
|
+
alignItems: 'flex-start',
|
|
138
|
+
},
|
|
139
|
+
trailingGroup: {
|
|
140
|
+
flexDirection: 'row',
|
|
141
|
+
alignItems: 'center',
|
|
142
|
+
gap: DS_SPACING[300],
|
|
143
|
+
flexShrink: 0,
|
|
144
|
+
},
|
|
145
|
+
trailingTextGroup: {
|
|
146
|
+
alignItems: 'flex-end',
|
|
147
|
+
gap: DS_SPACING[100],
|
|
148
|
+
},
|
|
149
|
+
trailingText: {
|
|
150
|
+
...DS_TYPOGRAPHY.bodySm,
|
|
151
|
+
color: DS_SEMANTIC.text.muted,
|
|
152
|
+
flexShrink: 1,
|
|
153
|
+
},
|
|
154
|
+
trailingSubtext: {
|
|
155
|
+
...DS_TYPOGRAPHY.labelXs,
|
|
156
|
+
color: DS_SEMANTIC.text.muted,
|
|
157
|
+
flexShrink: 1,
|
|
158
|
+
},
|
|
159
|
+
// Disabled text — a step lighter than `text.muted` (which the subtitle/footer already use for
|
|
160
|
+
// ordinary secondary text), so a disabled row reads as inactive rather than just secondary —
|
|
161
|
+
// same reasoning as InputField/SearchField (see DS_SEMANTIC.text.disabled's own doc comment).
|
|
162
|
+
disabledText: {
|
|
163
|
+
color: DS_SEMANTIC.text.disabled,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Loading } from './Loading';
|
|
4
|
+
import { DS_SEMANTIC } from '../../tokens';
|
|
5
|
+
|
|
6
|
+
export default defineCatalogPage({
|
|
7
|
+
component: 'Loading',
|
|
8
|
+
group: 'Feedback',
|
|
9
|
+
description: 'An indeterminate loader — the shape fills up, empties out, then fills again, seamlessly. Two variants: circle (used internally by Button and Pill for their own loading states) and linear.',
|
|
10
|
+
a11y: 'Exposed to assistive tech as accessibilityRole="progressbar" with accessibilityLabel="Loading" — no extra wiring needed at the call site.',
|
|
11
|
+
// One example per `variant` enum value — `circle` is the default, so it comes first.
|
|
12
|
+
variants: {
|
|
13
|
+
itemsFill: true,
|
|
14
|
+
items: [
|
|
15
|
+
{ key: 'circle', name: 'Circle', props: { variant: 'circle' }, node: <Loading variant="circle" /> },
|
|
16
|
+
{ key: 'linear', name: 'Linear', props: { variant: 'linear' }, node: <Loading variant="linear" /> },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
// `size` (circle) and `height` (linear) are continuous numbers, not enums — small/medium/large
|
|
20
|
+
// and thin/default/thick are explicit, labeled sweeps across each. Each sweep names its variant
|
|
21
|
+
// through `group`, so the page shows one row per variant; Accent colour applies to both and
|
|
22
|
+
// stays in "Other configurations".
|
|
23
|
+
states: {
|
|
24
|
+
items: [
|
|
25
|
+
{ key: 'small-circle', group: 'circle', name: 'Small circle', node: <Loading variant="circle" size={14} /> },
|
|
26
|
+
{ key: 'medium-circle', group: 'circle', name: 'Medium circle (default)', node: <Loading variant="circle" size={20} /> },
|
|
27
|
+
{ key: 'large-circle', group: 'circle', name: 'Large circle', node: <Loading variant="circle" size={40} /> },
|
|
28
|
+
{ key: 'thin-linear', group: 'linear', fill: true, name: 'Thin linear', node: <Loading variant="linear" height={2} /> },
|
|
29
|
+
{ key: 'default-linear', group: 'linear', fill: true, name: 'Default linear', node: <Loading variant="linear" height={4} /> },
|
|
30
|
+
{ key: 'thick-linear', group: 'linear', fill: true, name: 'Thick linear', node: <Loading variant="linear" height={8} /> },
|
|
31
|
+
{ key: 'accent', name: 'Accent colour', node: <Loading variant="circle" color={DS_SEMANTIC.emphasis.info} /> },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { View, Animated, Easing, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import Svg, { Path, type PathProps } from 'react-native-svg';
|
|
4
|
+
import { DS_SEMANTIC, DS_MOTION_LOOP_DURATION } from '../../tokens';
|
|
5
|
+
|
|
6
|
+
// `Animated.createAnimatedComponent` forces `collapsable: false` onto the wrapped component's
|
|
7
|
+
// props (so the native view isn't flattened out from under the native driver) — react-native-svg's
|
|
8
|
+
// web shim doesn't recognize `collapsable` and passes it straight through to the DOM, so an
|
|
9
|
+
// animated Path would otherwise log "Received `false` for a non-boolean attribute `collapsable`."
|
|
10
|
+
// This wrapper drops it before it reaches the real Path; native platforms never read it off Path.
|
|
11
|
+
const AnimatedPath = Animated.createAnimatedComponent(
|
|
12
|
+
React.forwardRef<unknown, PathProps & { collapsable?: boolean }>(({ collapsable: _collapsable, ...rest }, ref) => (
|
|
13
|
+
<Path ref={ref as never} {...rest} />
|
|
14
|
+
)),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// A 20×20 viewBox, ~292° arc (radius 9, centre 10,10), 2px stroke. The arc is a single stroke whose
|
|
18
|
+
// dash exactly covers its length, so animating the dash offset draws it on (fills) then off (empties).
|
|
19
|
+
const ARC_PATH =
|
|
20
|
+
'M19 10C19 11.9289 18.3803 13.8067 17.2323 15.3567C16.0843 16.9067 14.4687 18.0468 12.6236 18.6091C10.7785 19.1714 8.8016 19.126 6.98426 18.4797C5.16692 17.8334 3.6053 16.6203 2.52959 15.0193C1.45388 13.4182 0.920996 11.514 1.00948 9.58714C1.09796 7.66032 1.80313 5.81291 3.02105 4.3172C4.23897 2.8215 5.9052 1.75664 7.77412 1.2796C9.64305 0.802556 11.6158 0.938564 13.4016 1.66758';
|
|
21
|
+
const ARC_LEN = 45.9;
|
|
22
|
+
|
|
23
|
+
export type LoadingVariant = 'circle' | 'linear';
|
|
24
|
+
|
|
25
|
+
export interface LoadingProps {
|
|
26
|
+
/** Which shape to render. @default 'circle' */
|
|
27
|
+
variant?: LoadingVariant;
|
|
28
|
+
/** Diameter in px — circle only. @default 20 */
|
|
29
|
+
size?: number;
|
|
30
|
+
/** Track width — linear only. @default '100%' */
|
|
31
|
+
width?: number | `${number}%`;
|
|
32
|
+
/** Track/bar thickness — linear only. @default 4 */
|
|
33
|
+
height?: number;
|
|
34
|
+
/** Stroke (circle) / fill (linear) colour. Defaults to the regular text colour. */
|
|
35
|
+
color?: string;
|
|
36
|
+
/** Track (background) colour — linear only. */
|
|
37
|
+
trackColor?: string;
|
|
38
|
+
/** One fill→empty cycle duration in ms. @default DS_MOTION_LOOP_DURATION.spinner (1200) */
|
|
39
|
+
duration?: number;
|
|
40
|
+
style?: StyleProp<ViewStyle>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Indeterminate loader — the shape fills up, empties out, then fills again, seamlessly, in either of
|
|
45
|
+
* two variants:
|
|
46
|
+
* - `circle` (default): a dash slides across the arc's own length over a 2× period, so a single
|
|
47
|
+
* continuous timing produces the fill→empty→fill motion.
|
|
48
|
+
* - `linear`: a bar's width animates 0→100%→0% explicitly (two phases — there's no dash-slide
|
|
49
|
+
* equivalent for a plain width fill).
|
|
50
|
+
* Both are JS-driven (`useNativeDriver: false`), so Fabric-safe: a continuous loop on an
|
|
51
|
+
* always-mounted view, no imperative setValue. Used internally by Button and Pill for their own
|
|
52
|
+
* loading states (circle).
|
|
53
|
+
*/
|
|
54
|
+
export function Loading({
|
|
55
|
+
variant = 'circle',
|
|
56
|
+
size = 20,
|
|
57
|
+
width = '100%',
|
|
58
|
+
height = 4,
|
|
59
|
+
color = DS_SEMANTIC.text.regular,
|
|
60
|
+
trackColor = DS_SEMANTIC.surface.muted,
|
|
61
|
+
duration = DS_MOTION_LOOP_DURATION.spinner,
|
|
62
|
+
style,
|
|
63
|
+
}: LoadingProps) {
|
|
64
|
+
// Circle drives this directly as a dash offset (ARC_LEN..-ARC_LEN); linear reads it as 0..1 and
|
|
65
|
+
// interpolates to a width percentage. Different ranges, same underlying value — each variant's own
|
|
66
|
+
// effect below sets up the animation appropriate to it.
|
|
67
|
+
const progress = useRef(new Animated.Value(variant === 'circle' ? ARC_LEN : 0)).current;
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const loop =
|
|
71
|
+
variant === 'circle'
|
|
72
|
+
? Animated.loop(
|
|
73
|
+
Animated.timing(progress, {
|
|
74
|
+
toValue: -ARC_LEN,
|
|
75
|
+
duration,
|
|
76
|
+
easing: Easing.inOut(Easing.ease),
|
|
77
|
+
useNativeDriver: false,
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
: Animated.loop(
|
|
81
|
+
Animated.sequence([
|
|
82
|
+
Animated.timing(progress, { toValue: 1, duration, easing: Easing.inOut(Easing.ease), useNativeDriver: false }),
|
|
83
|
+
Animated.timing(progress, { toValue: 0, duration, easing: Easing.inOut(Easing.ease), useNativeDriver: false }),
|
|
84
|
+
]),
|
|
85
|
+
);
|
|
86
|
+
loop.start();
|
|
87
|
+
return () => loop.stop();
|
|
88
|
+
}, [variant, duration, progress]);
|
|
89
|
+
|
|
90
|
+
if (variant === 'linear') {
|
|
91
|
+
const fillWidth = progress.interpolate({ inputRange: [0, 1], outputRange: ['0%', '100%'] });
|
|
92
|
+
return (
|
|
93
|
+
<View
|
|
94
|
+
style={[styles.track, { width, height, backgroundColor: trackColor, borderRadius: height / 2 }, style]}
|
|
95
|
+
accessible
|
|
96
|
+
accessibilityRole="progressbar"
|
|
97
|
+
accessibilityLabel="Loading"
|
|
98
|
+
>
|
|
99
|
+
<Animated.View
|
|
100
|
+
style={[styles.fill, { height, backgroundColor: color, width: fillWidth, borderRadius: height / 2 }]}
|
|
101
|
+
/>
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
// The accessibility props live on this wrapping View, not the Svg: react-native-svg's web shim
|
|
108
|
+
// forwards unrecognized props straight to the DOM, and `accessible`/`accessibilityRole` would
|
|
109
|
+
// otherwise log "Received `true`/`\"progressbar\"` for a non-boolean/non-standard attribute."
|
|
110
|
+
<View style={style} accessible accessibilityRole="progressbar" accessibilityLabel="Loading">
|
|
111
|
+
<Svg width={size} height={size} viewBox="0 0 20 20" fill="none">
|
|
112
|
+
<AnimatedPath d={ARC_PATH} stroke={color} strokeWidth={2} strokeDasharray={ARC_LEN} strokeDashoffset={progress} />
|
|
113
|
+
</Svg>
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const styles = StyleSheet.create({
|
|
119
|
+
track: { overflow: 'hidden' },
|
|
120
|
+
fill: { position: 'absolute', left: 0, top: 0 },
|
|
121
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineCatalogPage, type ComparisonDef } from '@krapwoo/ds-viewer';
|
|
3
|
+
import { Pill } from './Pill';
|
|
4
|
+
|
|
5
|
+
// Grid comparisons: each cell is a real instance with exactly the props its row and column name.
|
|
6
|
+
// Never build these by multiplying `variants` with `states` — those are pre-rendered nodes and
|
|
7
|
+
// cannot combine.
|
|
8
|
+
function grid(
|
|
9
|
+
rowLabel: string,
|
|
10
|
+
columnLabel: string,
|
|
11
|
+
rows: { key: string; label: string }[],
|
|
12
|
+
columns: { key: string; label: string }[],
|
|
13
|
+
cell: (row: string, column: string) => React.ReactNode,
|
|
14
|
+
size?: ComparisonDef['size'],
|
|
15
|
+
): ComparisonDef {
|
|
16
|
+
return {
|
|
17
|
+
rowLabel,
|
|
18
|
+
columnLabel,
|
|
19
|
+
rows,
|
|
20
|
+
columns,
|
|
21
|
+
cells: rows.flatMap((row) => columns.map((column) => ({ rowKey: row.key, columnKey: column.key, node: cell(row.key, column.key) }))),
|
|
22
|
+
size,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Pill: Selection × State.
|
|
27
|
+
const PILL_COMPARISON = grid(
|
|
28
|
+
'Selection',
|
|
29
|
+
'State',
|
|
30
|
+
[
|
|
31
|
+
{ key: 'selected', label: 'Selected' },
|
|
32
|
+
{ key: 'not-selected', label: 'Not selected' },
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
{ key: 'icon-text', label: 'Icon + Text' },
|
|
36
|
+
{ key: 'icon-only', label: 'Icon-only' },
|
|
37
|
+
{ key: 'disabled', label: 'Disabled' },
|
|
38
|
+
{ key: 'loading', label: 'Loading' },
|
|
39
|
+
],
|
|
40
|
+
(row, column) => (
|
|
41
|
+
<Pill
|
|
42
|
+
label="Home"
|
|
43
|
+
variant={row === 'selected' ? 'selected' : 'not_selected'}
|
|
44
|
+
iconName="home"
|
|
45
|
+
showText={column !== 'icon-only'}
|
|
46
|
+
accessibilityLabel="Home"
|
|
47
|
+
disabled={column === 'disabled'}
|
|
48
|
+
loading={column === 'loading'}
|
|
49
|
+
onPress={() => {}}
|
|
50
|
+
/>
|
|
51
|
+
),
|
|
52
|
+
'compact',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export default defineCatalogPage({
|
|
56
|
+
component: 'Pill',
|
|
57
|
+
group: 'Actions',
|
|
58
|
+
specimenSize: 'compact',
|
|
59
|
+
comparison: PILL_COMPARISON,
|
|
60
|
+
description: 'A compact selectable chip — selected/unselected states with an optional leading icon.',
|
|
61
|
+
whenToUse: "A selection toggle, not an action — tapping it flips a persistent selected state. If tapping it should instead make something happen, use Button.",
|
|
62
|
+
a11y: 'Pressable with accessibilityLabel; an icon-only pill (showText={false}) needs an explicit accessibilityLabel so it is announced.',
|
|
63
|
+
variants: {
|
|
64
|
+
items: [
|
|
65
|
+
{ key: 'selected', name: 'Selected', props: { variant: 'selected' }, node: <Pill label="Home" variant="selected" iconName="home" onPress={() => {}} /> },
|
|
66
|
+
{ key: 'not-selected', name: 'Not selected', props: { variant: 'not_selected' }, node: <Pill label="Work" variant="not_selected" iconName="briefcase" onPress={() => {}} /> },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
states: {
|
|
70
|
+
items: [
|
|
71
|
+
{
|
|
72
|
+
key: 'icon-text',
|
|
73
|
+
name: 'Icon + Text',
|
|
74
|
+
node: <Pill label="Nearby" variant="not_selected" iconName="pin" onPress={() => {}} />,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'icon-only',
|
|
78
|
+
name: 'Icon-only',
|
|
79
|
+
node: <Pill label="Saved" variant="not_selected" iconName="bell" showText={false} onPress={() => {}} />,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: 'disabled',
|
|
83
|
+
name: 'Disabled',
|
|
84
|
+
node: <Pill label="Sold out" variant="not_selected" iconName="clock" disabled onPress={() => {}} />,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: 'loading',
|
|
88
|
+
name: 'Loading',
|
|
89
|
+
node: <Pill label="Saving" variant="not_selected" iconName="clock" loading onPress={() => {}} />,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Pressable, Text, StyleSheet, View } from 'react-native';
|
|
3
|
+
import { DS_SEMANTIC, DS_RADIUS, DS_SPACING, DS_TYPOGRAPHY, DS_SHADOW } from '../../tokens';
|
|
4
|
+
import { Icon } from '../../icons/Icon.native';
|
|
5
|
+
import { Loading } from '../Loading';
|
|
6
|
+
import type { PillProps } from './Pill.types';
|
|
7
|
+
|
|
8
|
+
function PillIcon({
|
|
9
|
+
name,
|
|
10
|
+
selected,
|
|
11
|
+
size = 16,
|
|
12
|
+
}: {
|
|
13
|
+
name: NonNullable<PillProps['iconName']>;
|
|
14
|
+
selected: boolean;
|
|
15
|
+
size?: number;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<Icon name={name} size={size} color={selected ? DS_SEMANTIC.text.inverse : DS_SEMANTIC.text.regular} />
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A single-line selection chip. `selected` fills solid dark with inverse content; `not_selected`
|
|
24
|
+
* sits on white with a resting shadow. Leading slot holds an `iconName`, a custom `icon` node
|
|
25
|
+
* (sizes naturally), or the default menu icon; the label can be hidden for an icon-only pill.
|
|
26
|
+
*/
|
|
27
|
+
export function Pill({
|
|
28
|
+
label = 'Text',
|
|
29
|
+
variant = 'selected',
|
|
30
|
+
accessibilityLabel,
|
|
31
|
+
showText = true,
|
|
32
|
+
iconName,
|
|
33
|
+
iconSize = 16,
|
|
34
|
+
icon,
|
|
35
|
+
onPress,
|
|
36
|
+
disabled = false,
|
|
37
|
+
loading = false,
|
|
38
|
+
style,
|
|
39
|
+
}: PillProps) {
|
|
40
|
+
const isSelected = variant === 'selected';
|
|
41
|
+
const isDisabled = disabled || loading;
|
|
42
|
+
const [focused, setFocused] = useState(false);
|
|
43
|
+
|
|
44
|
+
// A custom `icon` node (e.g. a badge) is taller than a plain icon. The pill keeps a fixed
|
|
45
|
+
// height regardless, so the badge centers in the same height as an icon+label pill; its horizontal
|
|
46
|
+
// padding is then reduced to equal the (smaller) vertical breathing room around the taller badge.
|
|
47
|
+
const isBadge = !iconName && icon != null;
|
|
48
|
+
|
|
49
|
+
// The leading slot sizes to its content, so it holds an `iconName` (at `iconSize`), a custom `icon`
|
|
50
|
+
// node of any size, or the default menu icon.
|
|
51
|
+
const iconNode = iconName ? (
|
|
52
|
+
<PillIcon name={iconName} selected={isSelected} size={iconSize} />
|
|
53
|
+
) : (
|
|
54
|
+
icon ?? <PillIcon name="menu" selected={isSelected} size={iconSize} />
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const content = loading ? (
|
|
58
|
+
<Loading
|
|
59
|
+
size={16}
|
|
60
|
+
color={isSelected ? DS_SEMANTIC.text.inverse : DS_SEMANTIC.text.regular}
|
|
61
|
+
/>
|
|
62
|
+
) : (
|
|
63
|
+
<>
|
|
64
|
+
<View style={styles.iconSlot}>{iconNode}</View>
|
|
65
|
+
{showText && label ? (
|
|
66
|
+
<Text
|
|
67
|
+
// Pills are single-line chips — never wrap (a sub-pixel-tight equal width would otherwise
|
|
68
|
+
// fold the label onto two lines and grow the pill).
|
|
69
|
+
numberOfLines={1}
|
|
70
|
+
style={[
|
|
71
|
+
styles.label,
|
|
72
|
+
{ color: isSelected ? DS_SEMANTIC.text.inverse : DS_SEMANTIC.text.regular },
|
|
73
|
+
]}
|
|
74
|
+
>
|
|
75
|
+
{label}
|
|
76
|
+
</Text>
|
|
77
|
+
) : null}
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Icon-only pills are forced to a fixed width equal to the pill's own fixed height (not left to
|
|
82
|
+
// emerge from padding + the icon's own width, which only happens to land on a square at the
|
|
83
|
+
// default iconSize=16 — passing a different `iconSize` would otherwise stretch it into a
|
|
84
|
+
// rounded rect instead of a true circle) — a fixed square, with content centred inside it via the
|
|
85
|
+
// container's own `alignItems`/`justifyContent: center`, keeps it circular at any icon size.
|
|
86
|
+
const containerStyle = [
|
|
87
|
+
styles.container,
|
|
88
|
+
showText ? { paddingHorizontal: isBadge ? DS_SPACING[400] : DS_SPACING[600] } : styles.iconOnly,
|
|
89
|
+
isSelected ? styles.selected : styles.notSelected,
|
|
90
|
+
isDisabled && styles.disabled,
|
|
91
|
+
style,
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// A selected pill is the current state — tapping it is a no-op, so it renders as a plain View
|
|
95
|
+
// (no pressed feedback) even when an onPress is wired up.
|
|
96
|
+
if (onPress && !isSelected) {
|
|
97
|
+
return (
|
|
98
|
+
<Pressable
|
|
99
|
+
onPress={onPress}
|
|
100
|
+
disabled={isDisabled}
|
|
101
|
+
// Extends the 40pt-tall pill to the 44pt minimum touch target. An icon-only pill is also only
|
|
102
|
+
// ~40pt wide (no label to widen it), so it needs the same padding on left/right too.
|
|
103
|
+
hitSlop={showText ? { top: 2, bottom: 2 } : { top: 2, bottom: 2, left: 2, right: 2 }}
|
|
104
|
+
accessibilityRole="button"
|
|
105
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
106
|
+
accessibilityState={{ disabled: isDisabled, busy: loading, selected: isSelected }}
|
|
107
|
+
onFocus={() => setFocused(true)}
|
|
108
|
+
onBlur={() => setFocused(false)}
|
|
109
|
+
style={({ pressed }) => [
|
|
110
|
+
containerStyle,
|
|
111
|
+
pressed && !isDisabled && styles.pressed,
|
|
112
|
+
focused && !isDisabled && styles.focused,
|
|
113
|
+
]}
|
|
114
|
+
>
|
|
115
|
+
{content}
|
|
116
|
+
</Pressable>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return <View style={containerStyle}>{content}</View>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const styles = StyleSheet.create({
|
|
124
|
+
container: {
|
|
125
|
+
flexDirection: 'row',
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
justifyContent: 'center',
|
|
128
|
+
gap: DS_SPACING[200],
|
|
129
|
+
// Fixed height so a tall leading node doesn't grow the pill past an icon+label one.
|
|
130
|
+
// Horizontal padding is applied per-instance (equal to the vertical breathing room): normal pills
|
|
131
|
+
// use 12, badge pills 8; icon-only pills get a fixed width instead (see `iconOnly` below).
|
|
132
|
+
height: DS_SPACING[2000],
|
|
133
|
+
borderRadius: DS_RADIUS.round,
|
|
134
|
+
},
|
|
135
|
+
// Fixed width equal to `container`'s own fixed height — a true circle regardless of the leading
|
|
136
|
+
// icon's size, rather than letting width emerge from padding + icon width (see the containerStyle
|
|
137
|
+
// comment above).
|
|
138
|
+
iconOnly: {
|
|
139
|
+
width: DS_SPACING[2000],
|
|
140
|
+
},
|
|
141
|
+
selected: {
|
|
142
|
+
// surface.inverse, not text.regular — this is a dark panel FILL, not ink; the two tokens share a
|
|
143
|
+
// value today but must be free to diverge in a rebrand.
|
|
144
|
+
backgroundColor: DS_SEMANTIC.surface.inverse,
|
|
145
|
+
// Transparent border matching not_selected's 2px, so both variants have identical layout boxes
|
|
146
|
+
// (otherwise unselected pills render taller and the row looks uneven). Reserved at the focus
|
|
147
|
+
// ring's own width (not 1px) so focusing only swaps the border colour, never its width — a
|
|
148
|
+
// width change on focus would otherwise shift the pill's layout, same fix as Button's border.
|
|
149
|
+
borderWidth: 2,
|
|
150
|
+
borderColor: 'transparent',
|
|
151
|
+
},
|
|
152
|
+
notSelected: {
|
|
153
|
+
backgroundColor: DS_SEMANTIC.surface.white,
|
|
154
|
+
// No border — the resting shadow alone separates the pill from the surface. Keep a transparent
|
|
155
|
+
// border (matching selected's reserved focus-ring width) so the box matches its height.
|
|
156
|
+
borderWidth: 2,
|
|
157
|
+
borderColor: 'transparent',
|
|
158
|
+
...DS_SHADOW.resting,
|
|
159
|
+
},
|
|
160
|
+
pressed: {
|
|
161
|
+
backgroundColor: DS_SEMANTIC.interaction.pressed,
|
|
162
|
+
},
|
|
163
|
+
focused: {
|
|
164
|
+
borderColor: DS_SEMANTIC.interaction.focused,
|
|
165
|
+
},
|
|
166
|
+
// Dims the WHOLE pill — container opacity multiplies down through every child in RN, so the
|
|
167
|
+
// label/icon dim with it; there is no way for a child to opt back out of an ancestor's opacity.
|
|
168
|
+
disabled: {
|
|
169
|
+
opacity: DS_SEMANTIC.interaction.disabledOpacity,
|
|
170
|
+
},
|
|
171
|
+
// Sizes to its content so the leading slot can hold an icon of any size or a custom node.
|
|
172
|
+
iconSlot: {
|
|
173
|
+
alignItems: 'center',
|
|
174
|
+
justifyContent: 'center',
|
|
175
|
+
},
|
|
176
|
+
// No pinned lineHeight — the pill's height follows the label typography (padding + natural line
|
|
177
|
+
// height), so a type-scale change resizes the pill instead of rattling inside a fixed box.
|
|
178
|
+
label: {
|
|
179
|
+
...DS_TYPOGRAPHY.labelSm,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import type { IconName } from '../../icons';
|
|
4
|
+
|
|
5
|
+
/** Selection state of the pill. */
|
|
6
|
+
export type PillVariant = 'selected' | 'not_selected';
|
|
7
|
+
|
|
8
|
+
export interface PillProps {
|
|
9
|
+
/** Chip text. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Selection state. */
|
|
12
|
+
variant?: PillVariant;
|
|
13
|
+
/** Screen-reader name. Strongly recommended for an icon-only pill (`showText={false}`) — it is not
|
|
14
|
+
* enforced, so omitting it means the placeholder `label` default gets announced instead. Defaults
|
|
15
|
+
* to `label` for text pills. */
|
|
16
|
+
accessibilityLabel?: string;
|
|
17
|
+
/** Hide the label for an icon-only pill. */
|
|
18
|
+
showText?: boolean;
|
|
19
|
+
/** Renders in the inverse/regular color based on selection (preferred over `icon`). */
|
|
20
|
+
iconName?: IconName;
|
|
21
|
+
/** Render size for `iconName` (default 16). */
|
|
22
|
+
iconSize?: number;
|
|
23
|
+
/** Custom leading node — sizes naturally; takes precedence over the default menu icon. */
|
|
24
|
+
icon?: ReactNode;
|
|
25
|
+
/** Tap handler. */
|
|
26
|
+
onPress?: () => void;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
loading?: boolean;
|
|
29
|
+
style?: StyleProp<ViewStyle>;
|
|
30
|
+
}
|