expo-interface 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -180
- package/package.json +39 -13
- package/src/alert/alert.css +49 -0
- package/src/alert/index.android.tsx +68 -0
- package/src/alert/index.ios.tsx +47 -0
- package/src/alert/index.tsx +60 -0
- package/src/alert/shared.ts +10 -0
- package/src/alert/types.ts +58 -0
- package/src/button/button.css +6 -0
- package/src/button/index.android.tsx +9 -2
- package/src/button/index.ios.tsx +8 -3
- package/src/button/index.tsx +19 -1
- package/src/button/types.ts +7 -0
- package/src/checkbox/checkbox.css +29 -0
- package/src/checkbox/index.android.tsx +54 -0
- package/src/checkbox/index.ios.tsx +50 -0
- package/src/checkbox/index.tsx +50 -0
- package/src/checkbox/types.ts +27 -0
- package/src/collapsible/collapsible.css +32 -0
- package/src/collapsible/index.android.tsx +34 -0
- package/src/collapsible/index.ios.tsx +31 -0
- package/src/collapsible/index.tsx +41 -0
- package/src/collapsible/shared.ts +21 -0
- package/src/collapsible/types.ts +24 -0
- package/src/color-picker/color-picker.css +66 -0
- package/src/color-picker/index.android.tsx +117 -0
- package/src/color-picker/index.ios.tsx +33 -0
- package/src/color-picker/index.tsx +62 -0
- package/src/color-picker/shared.ts +204 -0
- package/src/color-picker/sheet.tsx +432 -0
- package/src/color-picker/types.ts +34 -0
- package/src/context-menu/index.android.tsx +27 -0
- package/src/context-menu/index.ios.tsx +22 -0
- package/src/context-menu/index.tsx +56 -0
- package/src/date-time/index.android.tsx +3 -2
- package/src/divider/divider.css +16 -0
- package/src/divider/index.android.tsx +26 -0
- package/src/divider/index.ios.tsx +18 -0
- package/src/divider/index.tsx +21 -0
- package/src/divider/types.ts +20 -0
- package/src/field-group/field-group.css +23 -7
- package/src/field-group/index.android.tsx +22 -22
- package/src/field-group/index.web.tsx +2 -2
- package/src/gauge/gauge.css +200 -0
- package/src/gauge/index.android.tsx +212 -0
- package/src/gauge/index.ios.tsx +40 -0
- package/src/gauge/index.tsx +165 -0
- package/src/gauge/shared.ts +79 -0
- package/src/gauge/types.ts +64 -0
- package/src/index.ts +24 -3
- package/src/menu/index.android.tsx +66 -0
- package/src/menu/index.ios.tsx +72 -0
- package/src/menu/index.tsx +30 -0
- package/src/menu/list.tsx +98 -0
- package/src/menu/menu.css +83 -0
- package/src/menu/types.ts +60 -0
- package/src/picker/index.android.tsx +3 -2
- package/src/progress/index.android.tsx +23 -9
- package/src/progress/index.ios.tsx +7 -6
- package/src/progress/index.tsx +43 -6
- package/src/progress/progress.css +33 -0
- package/src/progress/types.ts +23 -6
- package/src/screen/header.tsx +1 -0
- package/src/segmented/index.android.tsx +67 -0
- package/src/segmented/index.ios.tsx +47 -0
- package/src/segmented/index.tsx +58 -0
- package/src/segmented/segmented.css +55 -0
- package/src/segmented/types.ts +31 -0
- package/src/slider/index.android.tsx +87 -0
- package/src/slider/index.ios.tsx +64 -0
- package/src/slider/index.tsx +55 -0
- package/src/slider/slider.css +30 -0
- package/src/slider/types.ts +39 -0
- package/src/stepper/index.android.tsx +58 -0
- package/src/stepper/index.ios.tsx +55 -0
- package/src/stepper/index.tsx +47 -0
- package/src/stepper/shared.ts +17 -0
- package/src/stepper/stepper.css +61 -0
- package/src/stepper/types.ts +35 -0
- package/src/switch/index.tsx +2 -0
- package/src/text-field/shared.ts +2 -0
- package/src/tooltip/index.android.tsx +23 -0
- package/src/tooltip/index.ios.tsx +17 -0
- package/src/tooltip/index.tsx +41 -0
- package/src/tooltip/tooltip.css +40 -0
- package/src/tooltip/types.ts +25 -0
- package/src/link.ts +0 -36
- package/src/qr/index.tsx +0 -26
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform hairline separator.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the SwiftUI `Divider` on iOS, the Jetpack Compose Material 3
|
|
5
|
+
* `HorizontalDivider` / `VerticalDivider` on Android, and the HTML `<hr>`
|
|
6
|
+
* element on web.
|
|
7
|
+
*/
|
|
8
|
+
export interface DividerProps {
|
|
9
|
+
/** Draw a vertical rule (for use inside a row) instead of a horizontal one. */
|
|
10
|
+
vertical?: boolean;
|
|
11
|
+
/** Line color. Defaults to the theme `separator` token. */
|
|
12
|
+
color?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Leading inset in points/dp, e.g. to align the rule with row content that
|
|
15
|
+
* sits after a leading icon.
|
|
16
|
+
*/
|
|
17
|
+
inset?: number;
|
|
18
|
+
/** Identifier used to locate the component in end-to-end tests. */
|
|
19
|
+
testID?: string;
|
|
20
|
+
}
|
|
@@ -7,12 +7,28 @@
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
|
-
* @expo/ui
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* @expo/ui paints inverted iOS grouped-list colors on web (grey page, white
|
|
11
|
+
* cards in light mode). Match native instead: the page is the screen
|
|
12
|
+
* background and the cards the raised element color, in both schemes, so the
|
|
13
|
+
* cards follow the kit palette rather than @expo/ui's own (which switches on
|
|
14
|
+
* `prefers-color-scheme` and can disagree with a forced scheme).
|
|
15
|
+
*
|
|
16
|
+
* The section card reads `--expo-ui-background`, so remap that variable here
|
|
17
|
+
* rather than matching react-native-web class names: those carry the property
|
|
18
|
+
* name (`r-borderRadius-…`) only in development and are hashed to `r-…` in a
|
|
19
|
+
* production build. Nothing else the kit renders inside a group reads it.
|
|
13
20
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
.field-group {
|
|
22
|
+
--expo-ui-background: var(--color-background-element);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* Row dividers: @expo/ui paints them in its gray-100, which is within a few
|
|
27
|
+
* shades of the card color above in both schemes. The selected-background
|
|
28
|
+
* token is one step away from the card in either scheme: visible, not heavy.
|
|
29
|
+
* `role="separator"` is the only stable hook; kit overlays inside a row (a
|
|
30
|
+
* Menu's own separator) carry `ui-*` classes and keep their color.
|
|
31
|
+
*/
|
|
32
|
+
.field-group [role='separator']:not([class^='ui-']) {
|
|
33
|
+
background-color: var(--color-background-selected);
|
|
18
34
|
}
|
|
@@ -59,9 +59,10 @@ function SectionFooter(props: FieldSectionFooterProps) {
|
|
|
59
59
|
function Section({children, title, titleUppercase = false, hidden}: FieldSectionProps) {
|
|
60
60
|
const card = useColor('backgroundElement');
|
|
61
61
|
const subtle = useColor('secondaryLabel');
|
|
62
|
+
const label = useColor('label');
|
|
62
63
|
if (hidden) return null;
|
|
63
64
|
|
|
64
|
-
const {header, footer, rows} = extractSlots(children);
|
|
65
|
+
const {header, footer, rows} = extractSlots(children, label);
|
|
65
66
|
const headerNode = header ?? (title ? (
|
|
66
67
|
<Text
|
|
67
68
|
color={subtle}
|
|
@@ -118,33 +119,33 @@ function cornerRadii(index: number, total: number) {
|
|
|
118
119
|
return {topStart: top, topEnd: top, bottomStart: bottom, bottomEnd: bottom};
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Pulls `SectionHeader`/`SectionFooter` slots out of a section's children.
|
|
124
|
+
* Empty children (`null`, booleans) are dropped, and raw strings or numbers
|
|
125
|
+
* become a `Text` row, since Compose slots cannot render them directly.
|
|
126
|
+
*/
|
|
127
|
+
function extractSlots(children: ReactNode, textColor: string) {
|
|
123
128
|
let header: ReactNode | undefined;
|
|
124
129
|
let footer: ReactNode | undefined;
|
|
125
130
|
const rows: ReactNode[] = [];
|
|
126
131
|
|
|
127
132
|
const walk = (node: ReactNode) => {
|
|
128
|
-
Children.
|
|
133
|
+
for (const child of Children.toArray(node)) {
|
|
129
134
|
if (!isValidElement(child)) {
|
|
130
|
-
rows.push(child);
|
|
131
|
-
|
|
135
|
+
rows.push(<Text color={textColor}>{String(child)}</Text>);
|
|
136
|
+
continue;
|
|
132
137
|
}
|
|
133
138
|
const props = child.props as {children?: ReactNode};
|
|
134
139
|
if (child.type === SectionHeader) {
|
|
135
140
|
header = props.children;
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
if (child.type === SectionFooter) {
|
|
141
|
+
} else if (child.type === SectionFooter) {
|
|
139
142
|
footer = props.children;
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
if (child.type === Fragment) {
|
|
143
|
+
} else if (child.type === Fragment) {
|
|
143
144
|
walk(props.children);
|
|
144
|
-
|
|
145
|
+
} else {
|
|
146
|
+
rows.push(child);
|
|
145
147
|
}
|
|
146
|
-
|
|
147
|
-
});
|
|
148
|
+
}
|
|
148
149
|
};
|
|
149
150
|
|
|
150
151
|
walk(children);
|
|
@@ -175,13 +176,12 @@ function groupChildren(children: ReactNode): ReactNode[] {
|
|
|
175
176
|
return;
|
|
176
177
|
}
|
|
177
178
|
if (isValidElement(child) && child.type === Fragment) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
179
|
+
// The recursion yields sections only (loose rows come back wrapped in
|
|
180
|
+
// their own implicit section), so they close any open implicit section.
|
|
181
|
+
const nested = groupChildren((child.props as {children?: ReactNode}).children);
|
|
182
|
+
if (nested.length > 0) {
|
|
183
|
+
flush();
|
|
184
|
+
result.push(...nested);
|
|
185
185
|
}
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
@@ -3,8 +3,8 @@ import {FieldGroup as BaseFieldGroup, type FieldGroupProps} from '@expo/ui';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Web hook for `field-group.css`. Clears the universal component's hardcoded
|
|
6
|
-
* scroll background so the wrapper supplies the app palette; section cards
|
|
7
|
-
*
|
|
6
|
+
* scroll background so the wrapper supplies the app palette; section cards
|
|
7
|
+
* and row dividers are recolored to the kit tokens in the CSS.
|
|
8
8
|
*/
|
|
9
9
|
function FieldGroup({style, ...props}: FieldGroupProps) {
|
|
10
10
|
return (
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SwiftUI gauge styles redrawn on web. Sizes are the points measured from
|
|
3
|
+
* iOS (see shared.ts); the unfilled tracks are iOS `tertiarySystemFill`
|
|
4
|
+
* (automatic) and `systemFill` (linearCapacity), which `light-dark()`
|
|
5
|
+
* resolves per scheme.
|
|
6
|
+
*/
|
|
7
|
+
.ui-gauge,
|
|
8
|
+
.ui-gauge-ring {
|
|
9
|
+
--ui-gauge-accent: var(--color-tint);
|
|
10
|
+
--ui-gauge-knockout: var(--color-background);
|
|
11
|
+
--ui-gauge-track: light-dark(rgba(118, 118, 128, 0.12), rgba(118, 118, 128, 0.24));
|
|
12
|
+
--ui-gauge-fill-track: light-dark(rgba(120, 120, 128, 0.2), rgba(120, 120, 128, 0.36));
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
font-family: var(--font-display);
|
|
15
|
+
font-size: 17px;
|
|
16
|
+
line-height: 22px;
|
|
17
|
+
color: var(--color-label);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.ui-gauge {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
align-items: center;
|
|
24
|
+
width: 100%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ui-gauge__label {
|
|
28
|
+
color: var(--color-label);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.ui-gauge__bound,
|
|
32
|
+
.ui-gauge__current {
|
|
33
|
+
color: var(--ui-gauge-accent);
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.ui-gauge__row {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: row;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: 8px;
|
|
42
|
+
width: 100%;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.ui-gauge__track {
|
|
46
|
+
position: relative;
|
|
47
|
+
display: block;
|
|
48
|
+
flex: 1;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
border-radius: 999px;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.ui-gauge__fill {
|
|
55
|
+
display: block;
|
|
56
|
+
height: 100%;
|
|
57
|
+
border-radius: 999px;
|
|
58
|
+
background: var(--ui-gauge-accent);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* automatic: thick capacity bar, label centered above, value centered below */
|
|
62
|
+
.ui-gauge--automatic .ui-gauge__label {
|
|
63
|
+
margin-bottom: 14px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ui-gauge--automatic .ui-gauge__track {
|
|
67
|
+
height: 15px;
|
|
68
|
+
background: var(--ui-gauge-track);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.ui-gauge--automatic .ui-gauge__current {
|
|
72
|
+
margin-top: 9px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* linear (accessoryLinear): tinted bar with a knocked-out point marker */
|
|
76
|
+
.ui-gauge--linear {
|
|
77
|
+
flex-direction: row;
|
|
78
|
+
gap: 8px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* The track is as tall as the marker so the knockout can overhang the bar. */
|
|
82
|
+
.ui-gauge--linear .ui-gauge__track {
|
|
83
|
+
height: 15px;
|
|
84
|
+
border-radius: 0;
|
|
85
|
+
overflow: visible;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.ui-gauge--linear .ui-gauge__track::before {
|
|
89
|
+
content: '';
|
|
90
|
+
position: absolute;
|
|
91
|
+
inset: 3.75px 0;
|
|
92
|
+
border-radius: 999px;
|
|
93
|
+
background: var(--ui-gauge-accent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.ui-gauge__marker {
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: 50%;
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: center;
|
|
102
|
+
width: 15px;
|
|
103
|
+
height: 15px;
|
|
104
|
+
border-radius: 50%;
|
|
105
|
+
background: var(--ui-gauge-knockout);
|
|
106
|
+
transform: translate(-50%, -50%);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.ui-gauge__dot {
|
|
110
|
+
width: 7.5px;
|
|
111
|
+
height: 7.5px;
|
|
112
|
+
border-radius: 50%;
|
|
113
|
+
background: var(--ui-gauge-accent);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
* linearCapacity (accessoryLinearCapacity): thin bar in a label/bar/value
|
|
118
|
+
* stack. The grid columns and rows are set inline per rendered label.
|
|
119
|
+
*/
|
|
120
|
+
.ui-gauge--linear-capacity {
|
|
121
|
+
display: grid;
|
|
122
|
+
align-items: center;
|
|
123
|
+
column-gap: 8px;
|
|
124
|
+
row-gap: 4px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.ui-gauge--linear-capacity .ui-gauge__label,
|
|
128
|
+
.ui-gauge--linear-capacity .ui-gauge__current {
|
|
129
|
+
justify-self: start;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ui-gauge--linear-capacity .ui-gauge__track {
|
|
133
|
+
height: 4px;
|
|
134
|
+
background: var(--ui-gauge-fill-track);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ui-gauge--linear-capacity .ui-gauge__current {
|
|
138
|
+
font-size: 12px;
|
|
139
|
+
line-height: 16px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* circular styles: 58pt ring drawn in SVG with HTML labels over it */
|
|
143
|
+
.ui-gauge-ring {
|
|
144
|
+
position: relative;
|
|
145
|
+
flex-shrink: 0;
|
|
146
|
+
display: inline-block;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.ui-gauge-ring__svg {
|
|
150
|
+
position: absolute;
|
|
151
|
+
inset: 0;
|
|
152
|
+
display: block;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.ui-gauge-ring__arc,
|
|
156
|
+
.ui-gauge-ring__fill {
|
|
157
|
+
stroke: var(--ui-gauge-accent);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.ui-gauge-ring__track {
|
|
161
|
+
stroke: var(--ui-gauge-accent);
|
|
162
|
+
opacity: 0.3;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.ui-gauge-ring__knockout {
|
|
166
|
+
fill: var(--ui-gauge-knockout);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.ui-gauge-ring__dot {
|
|
170
|
+
fill: var(--ui-gauge-accent);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.ui-gauge-ring__center {
|
|
174
|
+
position: absolute;
|
|
175
|
+
inset: 0;
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: center;
|
|
179
|
+
font-size: 24px;
|
|
180
|
+
line-height: 29px;
|
|
181
|
+
color: var(--ui-gauge-accent);
|
|
182
|
+
white-space: nowrap;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.ui-gauge-ring__center--label {
|
|
186
|
+
color: var(--color-label);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Centered 17px below the ring center (29 + 17 - half the 12px line). */
|
|
190
|
+
.ui-gauge-ring__bounds {
|
|
191
|
+
position: absolute;
|
|
192
|
+
top: 40px;
|
|
193
|
+
left: 12px;
|
|
194
|
+
display: flex;
|
|
195
|
+
justify-content: space-between;
|
|
196
|
+
width: 34px;
|
|
197
|
+
font-size: 10px;
|
|
198
|
+
line-height: 12px;
|
|
199
|
+
color: var(--ui-gauge-accent);
|
|
200
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type {GaugeProps} from './types';
|
|
2
|
+
import type {ModifierConfig} from '@expo/ui/jetpack-compose/modifiers';
|
|
3
|
+
|
|
4
|
+
import {useColorScheme} from 'react-native';
|
|
5
|
+
import {Box, CircularProgressIndicator, Column, Row, Spacer, Text, useMaterialColors} from '@expo/ui/jetpack-compose';
|
|
6
|
+
import {
|
|
7
|
+
alpha,
|
|
8
|
+
background,
|
|
9
|
+
clip,
|
|
10
|
+
fillMaxHeight,
|
|
11
|
+
fillMaxWidth,
|
|
12
|
+
height,
|
|
13
|
+
offset,
|
|
14
|
+
padding,
|
|
15
|
+
rotate,
|
|
16
|
+
Shapes,
|
|
17
|
+
size,
|
|
18
|
+
testID as testIDModifier,
|
|
19
|
+
weight,
|
|
20
|
+
width,
|
|
21
|
+
} from '@expo/ui/jetpack-compose/modifiers';
|
|
22
|
+
import {useColor} from '../theme';
|
|
23
|
+
import {fraction, gauge, markerOffset, track, withAlpha} from './shared';
|
|
24
|
+
|
|
25
|
+
const pill = Shapes.RoundedCorner(999);
|
|
26
|
+
const circle = Shapes.Circle;
|
|
27
|
+
const body = {fontSize: gauge.fontSize, lineHeight: gauge.lineHeight};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Android redraws each SwiftUI gauge style out of Compose primitives (the
|
|
31
|
+
* rings are Material 3 `CircularProgressIndicator`s, the bars clipped
|
|
32
|
+
* `Box`es) with the geometry measured from iOS. The live accent seed tints
|
|
33
|
+
* the indicator and the value labels, matching the iOS tint cascade; the
|
|
34
|
+
* descriptive label keeps the surface text color.
|
|
35
|
+
*/
|
|
36
|
+
export function Gauge({
|
|
37
|
+
value,
|
|
38
|
+
min = 0,
|
|
39
|
+
max = 1,
|
|
40
|
+
variant = 'automatic',
|
|
41
|
+
label,
|
|
42
|
+
currentValueLabel,
|
|
43
|
+
minimumValueLabel,
|
|
44
|
+
maximumValueLabel,
|
|
45
|
+
accentColor,
|
|
46
|
+
testID,
|
|
47
|
+
}: GaugeProps) {
|
|
48
|
+
const colors = useMaterialColors();
|
|
49
|
+
const scheme = useColorScheme() === 'dark' ? 'dark' : 'light';
|
|
50
|
+
const tint = useColor('tint');
|
|
51
|
+
const knockout = useColor('background');
|
|
52
|
+
const accent = accentColor ?? tint;
|
|
53
|
+
const f = fraction(value, min, max);
|
|
54
|
+
const testMods: ModifierConfig[] = testID ? [testIDModifier(testID)] : [];
|
|
55
|
+
|
|
56
|
+
const minText = minimumValueLabel != null
|
|
57
|
+
? <Text color={accent} style={body}>{minimumValueLabel}</Text>
|
|
58
|
+
: null;
|
|
59
|
+
const maxText = maximumValueLabel != null
|
|
60
|
+
? <Text color={accent} style={body}>{maximumValueLabel}</Text>
|
|
61
|
+
: null;
|
|
62
|
+
|
|
63
|
+
if (variant === 'circular' || variant === 'circularCapacity') {
|
|
64
|
+
const {size: ring, stroke} = gauge.ring;
|
|
65
|
+
const center = currentValueLabel ?? label;
|
|
66
|
+
const marker = markerOffset(f);
|
|
67
|
+
return (
|
|
68
|
+
<Box contentAlignment="center" modifiers={[size(ring, ring), ...testMods]}>
|
|
69
|
+
{variant === 'circular' ? (
|
|
70
|
+
<CircularProgressIndicator
|
|
71
|
+
progress={gauge.ring.arcSweep / 360}
|
|
72
|
+
color={accent}
|
|
73
|
+
trackColor="#00000000"
|
|
74
|
+
strokeWidth={stroke}
|
|
75
|
+
strokeCap="round"
|
|
76
|
+
gapSize={0}
|
|
77
|
+
modifiers={[size(ring, ring), rotate(gauge.ring.arcStart - 270)]}
|
|
78
|
+
/>
|
|
79
|
+
) : (
|
|
80
|
+
<CircularProgressIndicator
|
|
81
|
+
progress={f}
|
|
82
|
+
color={accent}
|
|
83
|
+
trackColor={withAlpha(accent, gauge.ring.trackOpacity)}
|
|
84
|
+
strokeWidth={stroke}
|
|
85
|
+
strokeCap="round"
|
|
86
|
+
gapSize={0}
|
|
87
|
+
modifiers={[size(ring, ring)]}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
{variant === 'circular' ? (
|
|
91
|
+
<Box
|
|
92
|
+
contentAlignment="center"
|
|
93
|
+
modifiers={[
|
|
94
|
+
offset(marker.x, marker.y),
|
|
95
|
+
size(gauge.ring.knockout, gauge.ring.knockout),
|
|
96
|
+
clip(circle),
|
|
97
|
+
background(knockout),
|
|
98
|
+
]}>
|
|
99
|
+
<Box modifiers={[size(gauge.ring.dot, gauge.ring.dot), clip(circle), background(accent)]}/>
|
|
100
|
+
</Box>
|
|
101
|
+
) : null}
|
|
102
|
+
{center != null ? (
|
|
103
|
+
<Text
|
|
104
|
+
color={currentValueLabel != null ? accent : colors.onSurface}
|
|
105
|
+
style={{fontSize: gauge.ring.centerFontSize, lineHeight: gauge.ring.centerLineHeight}}>
|
|
106
|
+
{center}
|
|
107
|
+
</Text>
|
|
108
|
+
) : null}
|
|
109
|
+
{variant === 'circular' && (minText || maxText) ? (
|
|
110
|
+
<Row
|
|
111
|
+
horizontalArrangement="spaceBetween"
|
|
112
|
+
verticalAlignment="center"
|
|
113
|
+
modifiers={[offset(0, gauge.ring.boundsOffset), width(gauge.ring.boundsWidth)]}>
|
|
114
|
+
{minimumValueLabel != null ? (
|
|
115
|
+
<Text color={accent} style={{fontSize: gauge.ring.boundsFontSize, lineHeight: gauge.ring.boundsLineHeight}}>
|
|
116
|
+
{minimumValueLabel}
|
|
117
|
+
</Text>
|
|
118
|
+
) : <Spacer/>}
|
|
119
|
+
{maximumValueLabel != null ? (
|
|
120
|
+
<Text color={accent} style={{fontSize: gauge.ring.boundsFontSize, lineHeight: gauge.ring.boundsLineHeight}}>
|
|
121
|
+
{maximumValueLabel}
|
|
122
|
+
</Text>
|
|
123
|
+
) : <Spacer/>}
|
|
124
|
+
</Row>
|
|
125
|
+
) : null}
|
|
126
|
+
</Box>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (variant === 'linear') {
|
|
131
|
+
const {bar, dot, knockout: knock} = gauge.linear;
|
|
132
|
+
return (
|
|
133
|
+
<Row verticalAlignment="center" horizontalArrangement={{spacedBy: gauge.rowGap}} modifiers={[fillMaxWidth(), ...testMods]}>
|
|
134
|
+
{minText}
|
|
135
|
+
<Box contentAlignment="centerStart" modifiers={[weight(1), height(knock)]}>
|
|
136
|
+
<Box modifiers={[fillMaxWidth(), height(bar), clip(pill), background(accent)]}/>
|
|
137
|
+
<Box modifiers={[fillMaxWidth(), fillMaxHeight(), padding(dot / 2, 0, dot / 2, 0)]}>
|
|
138
|
+
<Box contentAlignment="centerEnd" modifiers={[fillMaxWidth(f), fillMaxHeight()]}>
|
|
139
|
+
<Box
|
|
140
|
+
contentAlignment="center"
|
|
141
|
+
modifiers={[offset(knock / 2, 0), size(knock, knock), clip(circle), background(knockout)]}>
|
|
142
|
+
<Box modifiers={[size(dot, dot), clip(circle), background(accent)]}/>
|
|
143
|
+
</Box>
|
|
144
|
+
</Box>
|
|
145
|
+
</Box>
|
|
146
|
+
</Box>
|
|
147
|
+
{maxText}
|
|
148
|
+
</Row>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (variant === 'linearCapacity') {
|
|
153
|
+
const {bar, gap} = gauge.linearCapacity;
|
|
154
|
+
// Invisible copy of the bounds label so the stacked label and current
|
|
155
|
+
// value start at the bar's leading edge, as the SwiftUI layout does.
|
|
156
|
+
const ghost = minimumValueLabel != null
|
|
157
|
+
? <Text style={body} modifiers={[alpha(0)]}>{minimumValueLabel}</Text>
|
|
158
|
+
: null;
|
|
159
|
+
return (
|
|
160
|
+
<Column verticalArrangement={{spacedBy: gap}} modifiers={[fillMaxWidth(), ...testMods]}>
|
|
161
|
+
{label != null ? (
|
|
162
|
+
<Row horizontalArrangement={{spacedBy: gauge.rowGap}}>
|
|
163
|
+
{ghost}
|
|
164
|
+
<Text color={colors.onSurface} style={body}>{label}</Text>
|
|
165
|
+
</Row>
|
|
166
|
+
) : null}
|
|
167
|
+
<Row verticalAlignment="center" horizontalArrangement={{spacedBy: gauge.rowGap}} modifiers={[fillMaxWidth()]}>
|
|
168
|
+
{minText}
|
|
169
|
+
<Box contentAlignment="centerStart" modifiers={[weight(1), height(bar), clip(pill), background(track.linearCapacity[scheme])]}>
|
|
170
|
+
<Box modifiers={[fillMaxWidth(f), fillMaxHeight(), clip(pill), background(accent)]}/>
|
|
171
|
+
</Box>
|
|
172
|
+
{maxText}
|
|
173
|
+
</Row>
|
|
174
|
+
{currentValueLabel != null ? (
|
|
175
|
+
<Row horizontalArrangement={{spacedBy: gauge.rowGap}}>
|
|
176
|
+
{ghost}
|
|
177
|
+
<Text
|
|
178
|
+
color={accent}
|
|
179
|
+
style={{fontSize: gauge.linearCapacity.currentFontSize, lineHeight: gauge.linearCapacity.currentLineHeight}}>
|
|
180
|
+
{currentValueLabel}
|
|
181
|
+
</Text>
|
|
182
|
+
</Row>
|
|
183
|
+
) : null}
|
|
184
|
+
</Column>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const {bar, gapAbove, gapBelow} = gauge.automatic;
|
|
189
|
+
return (
|
|
190
|
+
<Column horizontalAlignment="center" modifiers={[fillMaxWidth(), ...testMods]}>
|
|
191
|
+
{label != null ? (
|
|
192
|
+
<>
|
|
193
|
+
<Text color={colors.onSurface} style={body}>{label}</Text>
|
|
194
|
+
<Spacer modifiers={[height(gapAbove)]}/>
|
|
195
|
+
</>
|
|
196
|
+
) : null}
|
|
197
|
+
<Row verticalAlignment="center" horizontalArrangement={{spacedBy: gauge.rowGap}} modifiers={[fillMaxWidth()]}>
|
|
198
|
+
{minText}
|
|
199
|
+
<Box contentAlignment="centerStart" modifiers={[weight(1), height(bar), clip(pill), background(track.automatic[scheme])]}>
|
|
200
|
+
<Box modifiers={[fillMaxWidth(f), fillMaxHeight(), clip(pill), background(accent)]}/>
|
|
201
|
+
</Box>
|
|
202
|
+
{maxText}
|
|
203
|
+
</Row>
|
|
204
|
+
{currentValueLabel != null ? (
|
|
205
|
+
<>
|
|
206
|
+
<Spacer modifiers={[height(gapBelow)]}/>
|
|
207
|
+
<Text color={accent} style={body}>{currentValueLabel}</Text>
|
|
208
|
+
</>
|
|
209
|
+
) : null}
|
|
210
|
+
</Column>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type {GaugeProps} from './types';
|
|
2
|
+
import type {ViewModifier} from '@expo/ui/swift-ui/modifiers';
|
|
3
|
+
|
|
4
|
+
import {Gauge as SwiftUIGauge, Text} from '@expo/ui/swift-ui';
|
|
5
|
+
import {gaugeStyle, tint} from '@expo/ui/swift-ui/modifiers';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* iOS renders SwiftUI's `Gauge` (iOS 16+) in the requested `gaugeStyle`. The
|
|
9
|
+
* Host-level `tint` cascade colors it with the accent seed; `accentColor`
|
|
10
|
+
* overrides that per instance. Each text label is a SwiftUI `Text` in the
|
|
11
|
+
* matching gauge slot, so SwiftUI picks the fonts and placement.
|
|
12
|
+
*/
|
|
13
|
+
export function Gauge({
|
|
14
|
+
value,
|
|
15
|
+
min = 0,
|
|
16
|
+
max = 1,
|
|
17
|
+
variant = 'automatic',
|
|
18
|
+
label,
|
|
19
|
+
currentValueLabel,
|
|
20
|
+
minimumValueLabel,
|
|
21
|
+
maximumValueLabel,
|
|
22
|
+
accentColor,
|
|
23
|
+
testID,
|
|
24
|
+
}: GaugeProps) {
|
|
25
|
+
const modifiers: ViewModifier[] = [gaugeStyle(variant)];
|
|
26
|
+
if (accentColor) modifiers.push(tint(accentColor));
|
|
27
|
+
return (
|
|
28
|
+
<SwiftUIGauge
|
|
29
|
+
value={value}
|
|
30
|
+
min={min}
|
|
31
|
+
max={max}
|
|
32
|
+
currentValueLabel={currentValueLabel != null ? <Text>{currentValueLabel}</Text> : undefined}
|
|
33
|
+
minimumValueLabel={minimumValueLabel != null ? <Text>{minimumValueLabel}</Text> : undefined}
|
|
34
|
+
maximumValueLabel={maximumValueLabel != null ? <Text>{maximumValueLabel}</Text> : undefined}
|
|
35
|
+
modifiers={modifiers}
|
|
36
|
+
testID={testID}>
|
|
37
|
+
{label != null ? <Text>{label}</Text> : undefined}
|
|
38
|
+
</SwiftUIGauge>
|
|
39
|
+
);
|
|
40
|
+
}
|