@isoline-ui/react-native 0.0.0-stage → 0.1.0-alpha.3
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/LICENSE +22 -0
- package/NOTICE.md +12 -0
- package/README.md +104 -3
- package/dist/amount.d.ts +3 -0
- package/dist/calendar.d.ts +7 -0
- package/dist/components/action-chip.d.ts +9 -0
- package/dist/components/amount-field.d.ts +12 -0
- package/dist/components/avatar.d.ts +17 -0
- package/dist/components/back-button.d.ts +12 -0
- package/dist/components/back-icon.d.ts +3 -0
- package/dist/components/badge.d.ts +13 -0
- package/dist/components/banner.d.ts +14 -0
- package/dist/components/bottom-sheet-native.d.ts +11 -0
- package/dist/components/bottom-sheet-web.d.ts +18 -0
- package/dist/components/bottom-sheet.d.ts +3 -0
- package/dist/components/button-group.d.ts +26 -0
- package/dist/components/button.d.ts +48 -0
- package/dist/components/checkbox.d.ts +12 -0
- package/dist/components/color-picker.d.ts +21 -0
- package/dist/components/context-menu.d.ts +26 -0
- package/dist/components/date-picker.d.ts +64 -0
- package/dist/components/dialog-context.d.ts +2 -0
- package/dist/components/dialog.d.ts +20 -0
- package/dist/components/empty-state.d.ts +15 -0
- package/dist/components/expand-indicator.d.ts +27 -0
- package/dist/components/expressive-shape.d.ts +18 -0
- package/dist/components/fab-menu-legacy.d.ts +31 -0
- package/dist/components/fab-menu.d.ts +21 -0
- package/dist/components/filter-chip.d.ts +69 -0
- package/dist/components/floating-action-button.d.ts +15 -0
- package/dist/components/icon-button.d.ts +40 -0
- package/dist/components/icons.d.ts +18 -0
- package/dist/components/input.d.ts +9 -0
- package/dist/components/keyboard-inset-view.d.ts +15 -0
- package/dist/components/list-controls.d.ts +38 -0
- package/dist/components/loading-indicator.d.ts +9 -0
- package/dist/components/menu-sheet.d.ts +36 -0
- package/dist/components/panel-sheet.d.ts +8 -0
- package/dist/components/password-visibility.d.ts +5 -0
- package/dist/components/popover-layout.d.ts +25 -0
- package/dist/components/popover.d.ts +20 -0
- package/dist/components/pressable.d.ts +12 -0
- package/dist/components/radio-navigation.d.ts +11 -0
- package/dist/components/refresh-control.d.ts +16 -0
- package/dist/components/row-gap.d.ts +9 -0
- package/dist/components/search-field.d.ts +22 -0
- package/dist/components/select-field.d.ts +16 -0
- package/dist/components/settings-list.d.ts +98 -0
- package/dist/components/settings-row.d.ts +13 -0
- package/dist/components/sheet-frame.d.ts +27 -0
- package/dist/components/sheet-lifecycle.d.ts +36 -0
- package/dist/components/sheet-transition.d.ts +36 -0
- package/dist/components/slider.d.ts +21 -0
- package/dist/components/snackbar.d.ts +10 -0
- package/dist/components/split-button.d.ts +38 -0
- package/dist/components/squish.d.ts +20 -0
- package/dist/components/swipeable-row.d.ts +20 -0
- package/dist/components/switch.d.ts +8 -0
- package/dist/components/tab-bar.d.ts +20 -0
- package/dist/components/text-field.d.ts +31 -0
- package/dist/components/text.d.ts +9 -0
- package/dist/components/time-picker.d.ts +40 -0
- package/dist/components/toolbar.d.ts +77 -0
- package/dist/components/top-bar.d.ts +27 -0
- package/dist/components/undo-bar.d.ts +23 -0
- package/dist/components/wavy-progress.d.ts +21 -0
- package/dist/components/zoomable-image.d.ts +18 -0
- package/dist/expressive-shapes.d.ts +8 -0
- package/dist/index.d.ts +62 -0
- package/dist/keyboard.d.ts +2 -0
- package/dist/material.d.ts +11 -0
- package/dist/media.d.ts +11 -0
- package/dist/motion.d.ts +96 -0
- package/dist/platform.d.ts +3 -0
- package/dist/preset-tokens.d.ts +264 -0
- package/dist/provider.d.ts +59 -0
- package/dist/reduced-motion.d.ts +45 -0
- package/dist/selection.d.ts +2 -0
- package/dist/shape.d.ts +99 -0
- package/dist/sheet-physics.d.ts +37 -0
- package/dist/theme.d.ts +103 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/use-keyboard-height.d.ts +10 -0
- package/dist/utils.d.ts +3 -0
- package/nativewind.cjs +68 -0
- package/package.json +63 -6
- package/src/amount.ts +53 -0
- package/src/calendar.ts +42 -0
- package/src/components/action-chip.tsx +64 -0
- package/src/components/amount-field.tsx +49 -0
- package/src/components/avatar.tsx +91 -0
- package/src/components/back-button.tsx +39 -0
- package/src/components/back-icon.tsx +25 -0
- package/src/components/badge.tsx +41 -0
- package/src/components/banner.tsx +63 -0
- package/src/components/bottom-sheet-native.tsx +52 -0
- package/src/components/bottom-sheet-web.tsx +193 -0
- package/src/components/bottom-sheet.tsx +5 -0
- package/src/components/button-group.tsx +326 -0
- package/src/components/button.tsx +337 -0
- package/src/components/checkbox.tsx +71 -0
- package/src/components/color-picker.tsx +115 -0
- package/src/components/context-menu.tsx +149 -0
- package/src/components/date-picker.tsx +466 -0
- package/src/components/dialog-context.ts +3 -0
- package/src/components/dialog.tsx +210 -0
- package/src/components/empty-state.tsx +49 -0
- package/src/components/expand-indicator.tsx +60 -0
- package/src/components/expressive-shape.tsx +37 -0
- package/src/components/fab-menu-legacy.tsx +181 -0
- package/src/components/fab-menu.tsx +90 -0
- package/src/components/filter-chip.tsx +136 -0
- package/src/components/floating-action-button.tsx +79 -0
- package/src/components/icon-button.tsx +127 -0
- package/src/components/icons.tsx +43 -0
- package/src/components/input.tsx +80 -0
- package/src/components/keyboard-inset-view.tsx +28 -0
- package/src/components/list-controls.tsx +134 -0
- package/src/components/loading-indicator.tsx +183 -0
- package/src/components/menu-sheet.tsx +133 -0
- package/src/components/panel-sheet.tsx +90 -0
- package/src/components/password-visibility.tsx +39 -0
- package/src/components/popover-layout.ts +47 -0
- package/src/components/popover.tsx +139 -0
- package/src/components/pressable.tsx +108 -0
- package/src/components/radio-navigation.ts +52 -0
- package/src/components/refresh-control.tsx +39 -0
- package/src/components/row-gap.tsx +13 -0
- package/src/components/search-field.tsx +92 -0
- package/src/components/select-field.tsx +105 -0
- package/src/components/settings-list.tsx +302 -0
- package/src/components/settings-row.tsx +61 -0
- package/src/components/sheet-frame.tsx +250 -0
- package/src/components/sheet-lifecycle.ts +233 -0
- package/src/components/sheet-transition.ts +105 -0
- package/src/components/slider.tsx +185 -0
- package/src/components/snackbar.tsx +97 -0
- package/src/components/split-button.tsx +212 -0
- package/src/components/squish.ts +41 -0
- package/src/components/swipeable-row.tsx +109 -0
- package/src/components/switch.tsx +77 -0
- package/src/components/tab-bar.tsx +195 -0
- package/src/components/text-field.tsx +264 -0
- package/src/components/text.tsx +58 -0
- package/src/components/time-picker.tsx +277 -0
- package/src/components/toolbar.tsx +406 -0
- package/src/components/top-bar.tsx +91 -0
- package/src/components/undo-bar.tsx +137 -0
- package/src/components/wavy-progress.tsx +150 -0
- package/src/components/zoomable-image.tsx +240 -0
- package/src/expressive-shapes.ts +25 -0
- package/src/index.ts +62 -0
- package/src/keyboard.ts +5 -0
- package/src/material.ts +16 -0
- package/src/media.ts +51 -0
- package/src/motion.ts +96 -0
- package/src/platform.ts +13 -0
- package/src/preset-tokens.ts +265 -0
- package/src/provider.tsx +201 -0
- package/src/reduced-motion.ts +50 -0
- package/src/selection.ts +11 -0
- package/src/shape.ts +217 -0
- package/src/sheet-physics.ts +73 -0
- package/src/theme.ts +180 -0
- package/src/tokens.ts +2 -0
- package/src/use-keyboard-height.ts +64 -0
- 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
|
+
}
|