@isoline-ui/react-native 0.0.0-stage → 1.0.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.
Files changed (167) hide show
  1. package/LICENSE +22 -0
  2. package/NOTICE.md +12 -0
  3. package/README.md +104 -3
  4. package/dist/amount.d.ts +3 -0
  5. package/dist/calendar.d.ts +7 -0
  6. package/dist/components/action-chip.d.ts +9 -0
  7. package/dist/components/amount-field.d.ts +12 -0
  8. package/dist/components/avatar.d.ts +17 -0
  9. package/dist/components/back-button.d.ts +12 -0
  10. package/dist/components/back-icon.d.ts +3 -0
  11. package/dist/components/badge.d.ts +13 -0
  12. package/dist/components/banner.d.ts +14 -0
  13. package/dist/components/bottom-sheet-native.d.ts +11 -0
  14. package/dist/components/bottom-sheet-web.d.ts +18 -0
  15. package/dist/components/bottom-sheet.d.ts +3 -0
  16. package/dist/components/button-group.d.ts +26 -0
  17. package/dist/components/button.d.ts +48 -0
  18. package/dist/components/checkbox.d.ts +12 -0
  19. package/dist/components/color-picker.d.ts +21 -0
  20. package/dist/components/context-menu.d.ts +26 -0
  21. package/dist/components/date-picker.d.ts +64 -0
  22. package/dist/components/dialog-context.d.ts +2 -0
  23. package/dist/components/dialog.d.ts +20 -0
  24. package/dist/components/empty-state.d.ts +15 -0
  25. package/dist/components/expand-indicator.d.ts +27 -0
  26. package/dist/components/expressive-shape.d.ts +18 -0
  27. package/dist/components/fab-menu-legacy.d.ts +31 -0
  28. package/dist/components/fab-menu.d.ts +21 -0
  29. package/dist/components/filter-chip.d.ts +69 -0
  30. package/dist/components/floating-action-button.d.ts +15 -0
  31. package/dist/components/icon-button.d.ts +40 -0
  32. package/dist/components/icons.d.ts +18 -0
  33. package/dist/components/input.d.ts +9 -0
  34. package/dist/components/keyboard-inset-view.d.ts +15 -0
  35. package/dist/components/list-controls.d.ts +38 -0
  36. package/dist/components/loading-indicator.d.ts +9 -0
  37. package/dist/components/menu-sheet.d.ts +36 -0
  38. package/dist/components/panel-sheet.d.ts +8 -0
  39. package/dist/components/password-visibility.d.ts +5 -0
  40. package/dist/components/popover-layout.d.ts +25 -0
  41. package/dist/components/popover.d.ts +20 -0
  42. package/dist/components/pressable.d.ts +12 -0
  43. package/dist/components/radio-navigation.d.ts +11 -0
  44. package/dist/components/refresh-control.d.ts +16 -0
  45. package/dist/components/row-gap.d.ts +9 -0
  46. package/dist/components/search-field.d.ts +22 -0
  47. package/dist/components/select-field.d.ts +16 -0
  48. package/dist/components/settings-list.d.ts +98 -0
  49. package/dist/components/settings-row.d.ts +13 -0
  50. package/dist/components/sheet-frame.d.ts +27 -0
  51. package/dist/components/sheet-lifecycle.d.ts +36 -0
  52. package/dist/components/sheet-transition.d.ts +36 -0
  53. package/dist/components/slider.d.ts +21 -0
  54. package/dist/components/snackbar.d.ts +10 -0
  55. package/dist/components/split-button.d.ts +38 -0
  56. package/dist/components/squish.d.ts +20 -0
  57. package/dist/components/swipeable-row.d.ts +20 -0
  58. package/dist/components/switch.d.ts +8 -0
  59. package/dist/components/tab-bar.d.ts +20 -0
  60. package/dist/components/text-field.d.ts +31 -0
  61. package/dist/components/text.d.ts +9 -0
  62. package/dist/components/time-picker.d.ts +40 -0
  63. package/dist/components/toolbar.d.ts +77 -0
  64. package/dist/components/top-bar.d.ts +27 -0
  65. package/dist/components/undo-bar.d.ts +23 -0
  66. package/dist/components/wavy-progress.d.ts +21 -0
  67. package/dist/components/zoomable-image.d.ts +18 -0
  68. package/dist/expressive-shapes.d.ts +8 -0
  69. package/dist/index.d.ts +62 -0
  70. package/dist/keyboard.d.ts +2 -0
  71. package/dist/material.d.ts +11 -0
  72. package/dist/media.d.ts +11 -0
  73. package/dist/motion.d.ts +96 -0
  74. package/dist/platform.d.ts +3 -0
  75. package/dist/preset-tokens.d.ts +264 -0
  76. package/dist/provider.d.ts +59 -0
  77. package/dist/reduced-motion.d.ts +45 -0
  78. package/dist/selection.d.ts +2 -0
  79. package/dist/shape.d.ts +99 -0
  80. package/dist/sheet-physics.d.ts +37 -0
  81. package/dist/theme.d.ts +103 -0
  82. package/dist/tokens.d.ts +2 -0
  83. package/dist/use-keyboard-height.d.ts +10 -0
  84. package/dist/utils.d.ts +3 -0
  85. package/nativewind.cjs +68 -0
  86. package/package.json +63 -6
  87. package/src/amount.ts +53 -0
  88. package/src/calendar.ts +42 -0
  89. package/src/components/action-chip.tsx +64 -0
  90. package/src/components/amount-field.tsx +49 -0
  91. package/src/components/avatar.tsx +91 -0
  92. package/src/components/back-button.tsx +39 -0
  93. package/src/components/back-icon.tsx +25 -0
  94. package/src/components/badge.tsx +41 -0
  95. package/src/components/banner.tsx +63 -0
  96. package/src/components/bottom-sheet-native.tsx +52 -0
  97. package/src/components/bottom-sheet-web.tsx +193 -0
  98. package/src/components/bottom-sheet.tsx +5 -0
  99. package/src/components/button-group.tsx +326 -0
  100. package/src/components/button.tsx +337 -0
  101. package/src/components/checkbox.tsx +71 -0
  102. package/src/components/color-picker.tsx +115 -0
  103. package/src/components/context-menu.tsx +149 -0
  104. package/src/components/date-picker.tsx +466 -0
  105. package/src/components/dialog-context.ts +3 -0
  106. package/src/components/dialog.tsx +210 -0
  107. package/src/components/empty-state.tsx +49 -0
  108. package/src/components/expand-indicator.tsx +60 -0
  109. package/src/components/expressive-shape.tsx +37 -0
  110. package/src/components/fab-menu-legacy.tsx +181 -0
  111. package/src/components/fab-menu.tsx +90 -0
  112. package/src/components/filter-chip.tsx +136 -0
  113. package/src/components/floating-action-button.tsx +79 -0
  114. package/src/components/icon-button.tsx +127 -0
  115. package/src/components/icons.tsx +43 -0
  116. package/src/components/input.tsx +80 -0
  117. package/src/components/keyboard-inset-view.tsx +28 -0
  118. package/src/components/list-controls.tsx +134 -0
  119. package/src/components/loading-indicator.tsx +183 -0
  120. package/src/components/menu-sheet.tsx +133 -0
  121. package/src/components/panel-sheet.tsx +90 -0
  122. package/src/components/password-visibility.tsx +39 -0
  123. package/src/components/popover-layout.ts +47 -0
  124. package/src/components/popover.tsx +139 -0
  125. package/src/components/pressable.tsx +108 -0
  126. package/src/components/radio-navigation.ts +52 -0
  127. package/src/components/refresh-control.tsx +39 -0
  128. package/src/components/row-gap.tsx +13 -0
  129. package/src/components/search-field.tsx +92 -0
  130. package/src/components/select-field.tsx +105 -0
  131. package/src/components/settings-list.tsx +302 -0
  132. package/src/components/settings-row.tsx +61 -0
  133. package/src/components/sheet-frame.tsx +250 -0
  134. package/src/components/sheet-lifecycle.ts +233 -0
  135. package/src/components/sheet-transition.ts +105 -0
  136. package/src/components/slider.tsx +185 -0
  137. package/src/components/snackbar.tsx +97 -0
  138. package/src/components/split-button.tsx +212 -0
  139. package/src/components/squish.ts +41 -0
  140. package/src/components/swipeable-row.tsx +109 -0
  141. package/src/components/switch.tsx +77 -0
  142. package/src/components/tab-bar.tsx +195 -0
  143. package/src/components/text-field.tsx +264 -0
  144. package/src/components/text.tsx +58 -0
  145. package/src/components/time-picker.tsx +277 -0
  146. package/src/components/toolbar.tsx +406 -0
  147. package/src/components/top-bar.tsx +91 -0
  148. package/src/components/undo-bar.tsx +137 -0
  149. package/src/components/wavy-progress.tsx +150 -0
  150. package/src/components/zoomable-image.tsx +240 -0
  151. package/src/expressive-shapes.ts +25 -0
  152. package/src/index.ts +62 -0
  153. package/src/keyboard.ts +5 -0
  154. package/src/material.ts +16 -0
  155. package/src/media.ts +51 -0
  156. package/src/motion.ts +96 -0
  157. package/src/platform.ts +13 -0
  158. package/src/preset-tokens.ts +265 -0
  159. package/src/provider.tsx +201 -0
  160. package/src/reduced-motion.ts +50 -0
  161. package/src/selection.ts +11 -0
  162. package/src/shape.ts +217 -0
  163. package/src/sheet-physics.ts +73 -0
  164. package/src/theme.ts +180 -0
  165. package/src/tokens.ts +2 -0
  166. package/src/use-keyboard-height.ts +64 -0
  167. package/src/utils.ts +7 -0
@@ -0,0 +1,92 @@
1
+ import * as React from 'react';
2
+ import { TextInput, View } from 'react-native';
3
+
4
+ import { IconButton } from './icon-button.tsx';
5
+ import { Search, X } from './icons.tsx';
6
+ import {
7
+ Surface,
8
+ type MaterialOptions,
9
+ useIsolinePlatform,
10
+ useThemeColors,
11
+ } from '../provider.tsx';
12
+ import { cn } from '../utils.ts';
13
+
14
+ export interface SearchFieldProps extends MaterialOptions {
15
+ value: string;
16
+ onChangeText: (value: string) => void;
17
+ placeholder: string;
18
+ accessibilityLabel: string;
19
+ /** Label for the clear button, which only appears once something is typed. */
20
+ clearAccessibilityLabel: string;
21
+ /** Opens the keyboard on mount — for a screen whose only job is this field. */
22
+ autoFocus?: boolean;
23
+ className?: string;
24
+ }
25
+
26
+ /**
27
+ * The app's filter-as-you-type field: a pill with a leading magnifier and a
28
+ * clear button that appears only once there is something to clear.
29
+ *
30
+ * Deliberately not built on `Input`, which is the form control — a labelled
31
+ * box with room underneath for an error. A filter box has no label, no
32
+ * validation and no submit; it just narrows the list under it.
33
+ */
34
+ export function SearchField({
35
+ value,
36
+ onChangeText,
37
+ placeholder,
38
+ accessibilityLabel,
39
+ clearAccessibilityLabel,
40
+ autoFocus = false,
41
+ className,
42
+ material = 'solid',
43
+ glassStyle,
44
+ tintColor,
45
+ fallbackColor,
46
+ interactive = false,
47
+ }: SearchFieldProps) {
48
+ const platform = useIsolinePlatform();
49
+ const ios = platform === 'ios';
50
+ const materialActive = platform === 'ios' && material !== 'solid';
51
+ const Container = materialActive ? Surface : View;
52
+ const colors = useThemeColors();
53
+
54
+ return (
55
+ <Container
56
+ {...(materialActive
57
+ ? { material, glassStyle, tintColor, fallbackColor, interactive }
58
+ : {})}
59
+ className={cn(
60
+ 'h-12 flex-row items-center gap-2 rounded-full pl-4 pr-1.5',
61
+ !materialActive && 'bg-surfaceContainerHigh',
62
+ className,
63
+ )}
64
+ style={ios ? { borderRadius: 10, height: 44 } : undefined}
65
+ >
66
+ <Search size={18} color={colors.onSurfaceVariant} />
67
+
68
+ <TextInput
69
+ value={value}
70
+ onChangeText={onChangeText}
71
+ autoFocus={autoFocus}
72
+ placeholder={placeholder}
73
+ placeholderTextColor={colors.onSurfaceVariant}
74
+ className="h-12 flex-1 p-0 text-base text-foreground"
75
+ autoCapitalize="none"
76
+ autoCorrect={false}
77
+ returnKeyType="search"
78
+ accessibilityLabel={accessibilityLabel}
79
+ />
80
+
81
+ {value.length > 0 ? (
82
+ <IconButton
83
+ icon={X}
84
+ onPress={() => onChangeText('')}
85
+ size={36}
86
+ iconSize={18}
87
+ accessibilityLabel={clearAccessibilityLabel}
88
+ />
89
+ ) : null}
90
+ </Container>
91
+ );
92
+ }
@@ -0,0 +1,105 @@
1
+ import * as React from 'react';
2
+ import { View } from 'react-native';
3
+ import { ChevronDown } from './icons.tsx';
4
+ import { Popover } from './popover.tsx';
5
+ import { ContextMenuRows } from './context-menu.tsx';
6
+ import { Pressable } from './pressable.tsx';
7
+ import { Text } from './text.tsx';
8
+ import {
9
+ Surface,
10
+ useIsolinePlatform,
11
+ useThemeColors,
12
+ useRipple,
13
+ type MaterialOptions,
14
+ } from '../provider.tsx';
15
+ export interface SelectOption<T extends string> {
16
+ id: T;
17
+ label: string;
18
+ disabled?: boolean;
19
+ }
20
+ export interface SelectFieldProps<T extends string> extends MaterialOptions {
21
+ value: T;
22
+ options: SelectOption<T>[];
23
+ onChange: (value: T) => void;
24
+ accessibilityLabel: string;
25
+ menuMaterial?: MaterialOptions['material'];
26
+ disabled?: boolean;
27
+ }
28
+ export function SelectField<T extends string>({
29
+ value,
30
+ options,
31
+ onChange,
32
+ accessibilityLabel,
33
+ disabled,
34
+ material = 'solid',
35
+ menuMaterial = 'auto',
36
+ glassStyle,
37
+ tintColor,
38
+ fallbackColor,
39
+ interactive = false,
40
+ }: SelectFieldProps<T>) {
41
+ const platform = useIsolinePlatform();
42
+ const ios = platform === 'ios';
43
+ const glass = ios && material !== 'solid';
44
+ const colors = useThemeColors();
45
+ const ripple = useRipple();
46
+ const [open, setOpen] = React.useState(false);
47
+ const anchor = React.useRef<React.ComponentRef<typeof View>>(null);
48
+ const selected = options.find((option) => option.id === value);
49
+ const Container = glass ? Surface : View;
50
+ return (
51
+ <View ref={anchor} collapsable={false}>
52
+ <Container
53
+ {...(glass ? { material, glassStyle, tintColor, fallbackColor, interactive } : {})}
54
+ style={{ borderRadius: ios ? 10 : 24 }}
55
+ >
56
+ <Pressable
57
+ disabled={disabled}
58
+ accessibilityRole="button"
59
+ accessibilityLabel={accessibilityLabel}
60
+ accessibilityState={{ expanded: open, disabled }}
61
+ onPress={() => setOpen(true)}
62
+ android_ripple={{ color: ripple.onNeutral }}
63
+ style={{
64
+ minHeight: 44,
65
+ paddingHorizontal: 12,
66
+ paddingVertical: 8,
67
+ borderRadius: ios ? 10 : 24,
68
+ borderWidth: ios ? 0 : 1,
69
+ borderColor: colors.outline,
70
+ flexDirection: 'row',
71
+ alignItems: 'center',
72
+ gap: 6,
73
+ ...(!glass && ios ? { backgroundColor: colors.surfaceContainerHigh } : {}),
74
+ opacity: disabled ? 0.4 : 1,
75
+ }}
76
+ >
77
+ <Text style={{ fontSize: 15, color: colors.onSurface }}>
78
+ {selected?.label ?? value}
79
+ </Text>
80
+ <ChevronDown size={16} color={colors.onSurfaceVariant} />
81
+ </Pressable>
82
+ </Container>
83
+ <Popover
84
+ visible={open}
85
+ onClose={() => setOpen(false)}
86
+ anchorRef={anchor}
87
+ accessibilityLabel={accessibilityLabel}
88
+ material={menuMaterial}
89
+ glassStyle={glassStyle}
90
+ tintColor={tintColor}
91
+ fallbackColor={fallbackColor}
92
+ interactive={interactive}
93
+ >
94
+ <ContextMenuRows
95
+ items={options.map((option) => ({
96
+ ...option,
97
+ selected: option.id === value,
98
+ onPress: () => onChange(option.id),
99
+ }))}
100
+ onClose={() => setOpen(false)}
101
+ />
102
+ </Popover>
103
+ </View>
104
+ );
105
+ }
@@ -0,0 +1,302 @@
1
+ import * as React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Check, ChevronRight, type IconProps } from './icons.tsx';
5
+ import { Pressable } from './pressable.tsx';
6
+ import { RowGap } from './row-gap.tsx';
7
+ import { SelectField, type SelectOption } from './select-field.tsx';
8
+ import { Switch } from './switch.tsx';
9
+ import { Text } from './text.tsx';
10
+ import { useRipple, useThemeColors, type MaterialOptions } from '../provider.tsx';
11
+ import { cn } from '../utils.ts';
12
+
13
+ /**
14
+ * The pieces every settings screen is built from.
15
+ *
16
+ * Settings used to be one screen that spelled out each card, row, switch and
17
+ * chevron inline, which is why the same twelve lines of markup appeared a
18
+ * dozen times over and drifted apart as they were edited. Splitting settings
19
+ * across several screens would have multiplied that, so the row shapes live
20
+ * here instead and a screen only says what its settings *are*.
21
+ */
22
+
23
+ export type SettingsIcon = React.ComponentType<IconProps>;
24
+
25
+ /**
26
+ * Corner radii for one row of a connected group.
27
+ *
28
+ * M3 Expressive draws a settings group as separate cards that still read as
29
+ * one object: the group's outer corners stay large, and every corner facing a
30
+ * neighbour tightens to the small step.
31
+ */
32
+ export function groupRounding(index: number, count: number): string {
33
+ if (count === 1) return 'rounded-3xl';
34
+ if (index === 0) return 'rounded-t-3xl rounded-b-lg';
35
+ if (index === count - 1) return 'rounded-t-lg rounded-b-3xl';
36
+ return 'rounded-lg';
37
+ }
38
+
39
+ export function SettingsSection({
40
+ title,
41
+ children,
42
+ }: {
43
+ title?: string;
44
+ children: React.ReactNode;
45
+ }) {
46
+ return (
47
+ <View className="gap-2">
48
+ {title ? (
49
+ <Text variant="label" className="px-4">
50
+ {title}
51
+ </Text>
52
+ ) : null}
53
+ {children}
54
+ </View>
55
+ );
56
+ }
57
+
58
+ /**
59
+ * A run of rows drawn as one connected card group.
60
+ *
61
+ * Takes the rows as children rather than as data so a screen can drop one on a
62
+ * condition (`{Platform.OS === 'android' ? <Row/> : null}`) and still get the
63
+ * rounding right — a falsy child is discarded here, before the corners are
64
+ * worked out, so a hidden row never leaves a flattened corner behind.
65
+ */
66
+ export function SettingsGroup({ children }: { children: React.ReactNode }) {
67
+ const rows = React.Children.toArray(children).filter(React.isValidElement);
68
+
69
+ return (
70
+ <View className="mx-4">
71
+ {rows.map((row, index) => (
72
+ <React.Fragment key={row.key ?? index}>
73
+ {index > 0 ? <RowGap /> : null}
74
+ <View
75
+ className={cn(
76
+ 'overflow-hidden bg-surfaceContainerLow',
77
+ groupRounding(index, rows.length),
78
+ )}
79
+ >
80
+ {row}
81
+ </View>
82
+ </React.Fragment>
83
+ ))}
84
+ </View>
85
+ );
86
+ }
87
+
88
+ function RowLabel({
89
+ icon: Icon,
90
+ leading,
91
+ label,
92
+ description,
93
+ destructive,
94
+ }: {
95
+ icon?: SettingsIcon;
96
+ leading?: React.ReactNode;
97
+ label: string;
98
+ description?: string;
99
+ destructive?: boolean;
100
+ }) {
101
+ const colors = useThemeColors();
102
+
103
+ return (
104
+ <View className={cn('flex-1 flex-row gap-3', description ? 'items-start' : 'items-center')}>
105
+ {leading ? (
106
+ <View
107
+ className="h-[18px] w-[18px] items-center justify-center"
108
+ style={description ? { marginTop: 2 } : undefined}
109
+ >
110
+ {leading}
111
+ </View>
112
+ ) : Icon ? (
113
+ <Icon
114
+ size={18}
115
+ color={destructive ? colors.error : colors.onSurfaceVariant}
116
+ style={description ? { marginTop: 2 } : undefined}
117
+ />
118
+ ) : null}
119
+ <View className="flex-1">
120
+ <Text className={cn('text-[15px] font-medium', destructive && 'text-destructive')}>
121
+ {label}
122
+ </Text>
123
+ {description ? <Text variant="caption">{description}</Text> : null}
124
+ </View>
125
+ </View>
126
+ );
127
+ }
128
+
129
+ export interface SettingsNavRowProps {
130
+ icon?: SettingsIcon;
131
+ /** Custom leading content in the icon's place — an account's color dot. */
132
+ leading?: React.ReactNode;
133
+ label: string;
134
+ description?: string;
135
+ /** The current choice, shown at the row's trailing edge. */
136
+ value?: string;
137
+ onPress: () => void;
138
+ accessibilityLabel?: string;
139
+ /** For a row that removes something: red label, and no chevron to follow. */
140
+ destructive?: boolean;
141
+ }
142
+
143
+ /** A row that opens something else — another screen, or a picker sheet. */
144
+ export function SettingsNavRow({
145
+ icon,
146
+ leading,
147
+ label,
148
+ description,
149
+ value,
150
+ onPress,
151
+ accessibilityLabel,
152
+ destructive,
153
+ }: SettingsNavRowProps) {
154
+ const colors = useThemeColors();
155
+ const ripple = useRipple();
156
+
157
+ return (
158
+ <Pressable
159
+ onPress={onPress}
160
+ android_ripple={{ color: ripple.onNeutral }}
161
+ accessibilityRole="button"
162
+ accessibilityLabel={accessibilityLabel ?? label}
163
+ className="flex-row items-center gap-3 px-4 py-3.5"
164
+ >
165
+ <RowLabel
166
+ icon={icon}
167
+ leading={leading}
168
+ label={label}
169
+ description={description}
170
+ destructive={destructive}
171
+ />
172
+ {value ? (
173
+ <Text variant="muted" numberOfLines={1} className="max-w-[45%]">
174
+ {value}
175
+ </Text>
176
+ ) : null}
177
+ {destructive ? null : <ChevronRight size={18} color={colors.onSurfaceVariant} />}
178
+ </Pressable>
179
+ );
180
+ }
181
+
182
+ export interface SettingsChoiceRowProps {
183
+ /** Custom leading content — a calendar's color dot. */
184
+ leading?: React.ReactNode;
185
+ label: string;
186
+ description?: string;
187
+ selected: boolean;
188
+ onPress: () => void;
189
+ accessibilityLabel?: string;
190
+ }
191
+
192
+ /**
193
+ * One option of a choice spelled out as rows rather than a menu, for options
194
+ * that need a second line to tell apart: two calendars both called "Personal".
195
+ */
196
+ export function SettingsChoiceRow({
197
+ leading,
198
+ label,
199
+ description,
200
+ selected,
201
+ onPress,
202
+ accessibilityLabel,
203
+ }: SettingsChoiceRowProps) {
204
+ const colors = useThemeColors();
205
+ const ripple = useRipple();
206
+
207
+ return (
208
+ <Pressable
209
+ onPress={onPress}
210
+ android_ripple={{ color: ripple.onNeutral }}
211
+ accessibilityRole="radio"
212
+ accessibilityState={{ checked: selected }}
213
+ accessibilityLabel={accessibilityLabel ?? label}
214
+ className="flex-row items-center gap-3 px-4 py-3.5"
215
+ >
216
+ <RowLabel leading={leading} label={label} description={description} />
217
+ <View className="h-[18px] w-[18px]">
218
+ {selected ? <Check size={18} color={colors.primary} /> : null}
219
+ </View>
220
+ </Pressable>
221
+ );
222
+ }
223
+
224
+ export interface SettingsSwitchRowProps {
225
+ icon?: SettingsIcon;
226
+ /** Custom leading content in the icon's place — a calendar's color dot. */
227
+ leading?: React.ReactNode;
228
+ label: string;
229
+ description?: string;
230
+ value: boolean;
231
+ onValueChange: (value: boolean) => void;
232
+ disabled?: boolean;
233
+ accessibilityLabel?: string;
234
+ }
235
+
236
+ export function SettingsSwitchRow({
237
+ icon,
238
+ leading,
239
+ label,
240
+ description,
241
+ value,
242
+ onValueChange,
243
+ disabled,
244
+ accessibilityLabel,
245
+ }: SettingsSwitchRowProps) {
246
+ return (
247
+ <View className="flex-row items-center gap-3 px-4 py-3.5">
248
+ <RowLabel icon={icon} leading={leading} label={label} description={description} />
249
+ <Switch
250
+ value={value}
251
+ onValueChange={onValueChange}
252
+ disabled={disabled}
253
+ accessibilityLabel={accessibilityLabel ?? label}
254
+ />
255
+ </View>
256
+ );
257
+ }
258
+
259
+ export interface SettingsSelectRowProps<T extends string> extends MaterialOptions {
260
+ menuMaterial?: MaterialOptions['material'];
261
+ disabled?: boolean;
262
+ icon?: SettingsIcon;
263
+ label: string;
264
+ description?: string;
265
+ value: T;
266
+ options: SelectOption<T>[];
267
+ onChange: (value: T) => void;
268
+ accessibilityLabel?: string;
269
+ }
270
+
271
+ export function SettingsSelectRow<T extends string>({
272
+ icon,
273
+ label,
274
+ description,
275
+ value,
276
+ options,
277
+ onChange,
278
+ accessibilityLabel,
279
+ ...materialOptions
280
+ }: SettingsSelectRowProps<T>) {
281
+ return (
282
+ <View className="flex-row items-center gap-3 px-4 py-3.5">
283
+ <RowLabel icon={icon} label={label} description={description} />
284
+ <SelectField
285
+ {...materialOptions}
286
+ value={value}
287
+ options={options}
288
+ onChange={onChange}
289
+ accessibilityLabel={accessibilityLabel ?? label}
290
+ />
291
+ </View>
292
+ );
293
+ }
294
+
295
+ /**
296
+ * A card holding something that is not a row — the theme picker's button
297
+ * group, the accent swatches. Matches a group of one so custom content sits on
298
+ * the same surface at the same inset as the rows above and below it.
299
+ */
300
+ export function SettingsCard({ children }: { children: React.ReactNode }) {
301
+ return <View className="mx-4 gap-4 rounded-3xl bg-surfaceContainerLow p-4">{children}</View>;
302
+ }
@@ -0,0 +1,61 @@
1
+ import * as React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { ChevronRight, type IconProps } from './icons.tsx';
5
+ import { Pressable } from './pressable.tsx';
6
+ import { Switch } from './switch.tsx';
7
+ import { Text } from './text.tsx';
8
+ import { useRipple, useThemeColors } from '../provider.tsx';
9
+ import { cn } from '../utils.ts';
10
+
11
+ /** A row in a settings card: a symbol in its own tinted circle, a label and what it is set to. */
12
+ export function SettingsRow({
13
+ icon: Icon,
14
+ label,
15
+ description,
16
+ onPress,
17
+ value,
18
+ onValueChange,
19
+ first,
20
+ }: {
21
+ icon?: React.ComponentType<IconProps>;
22
+ label: string;
23
+ description?: string;
24
+ onPress?: () => void;
25
+ /** A switch instead of a chevron. */
26
+ value?: boolean;
27
+ onValueChange?: (value: boolean) => void;
28
+ first?: boolean;
29
+ }) {
30
+ const colors = useThemeColors();
31
+ const ripple = useRipple();
32
+ const toggle = value !== undefined && onValueChange;
33
+ return (
34
+ <Pressable
35
+ onPress={toggle ? () => onValueChange(!value) : onPress}
36
+ disabled={!toggle && !onPress}
37
+ android_ripple={{ color: ripple.onNeutral }}
38
+ accessibilityRole={toggle ? 'switch' : 'button'}
39
+ accessibilityState={toggle ? { checked: value } : undefined}
40
+ className={cn(
41
+ 'min-h-16 flex-row items-center gap-4 px-4 py-3',
42
+ !first && 'border-t border-border',
43
+ )}
44
+ >
45
+ {Icon ? (
46
+ <View className="h-10 w-10 items-center justify-center rounded-full bg-secondaryContainer">
47
+ <Icon size={20} color={colors.onSecondaryContainer} />
48
+ </View>
49
+ ) : null}
50
+ <View className="flex-1 gap-0.5">
51
+ <Text variant="subject">{label}</Text>
52
+ {description ? <Text variant="muted">{description}</Text> : null}
53
+ </View>
54
+ {toggle ? (
55
+ <Switch value={value} onValueChange={onValueChange} accessibilityLabel={label} />
56
+ ) : onPress ? (
57
+ <ChevronRight size={20} color={colors.outline} />
58
+ ) : null}
59
+ </Pressable>
60
+ );
61
+ }