jfs-components 0.0.79 → 0.0.85
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/CHANGELOG.md +29 -0
- package/lib/commonjs/components/AppBar/AppBar.js +70 -6
- package/lib/commonjs/components/AreaLineChart/AreaLineChart.js +866 -0
- package/lib/commonjs/components/AreaLineChart/chartMath.js +252 -0
- package/lib/commonjs/components/Attached/Attached.js +76 -7
- package/lib/commonjs/components/BubbleChart/BubbleChart.js +191 -0
- package/lib/commonjs/components/BubbleChart/bubblePacking.js +378 -0
- package/lib/commonjs/components/Checkbox/Checkbox.js +18 -2
- package/lib/commonjs/components/ClusterBubble/ClusterBubble.js +272 -0
- package/lib/commonjs/components/Drawer/Drawer.js +6 -1
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -6
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
- package/lib/commonjs/components/FormField/FormField.js +1 -14
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +5 -1
- package/lib/commonjs/components/ListItem/ListItem.js +6 -11
- package/lib/commonjs/components/MessageField/MessageField.js +1 -13
- package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +7 -1
- package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +12 -9
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +69 -160
- package/lib/commonjs/components/Spinner/Spinner.js +217 -0
- package/lib/commonjs/components/TextInput/TextInput.js +33 -18
- package/lib/commonjs/components/index.js +34 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/components/IconArrowdown.js +19 -0
- package/lib/commonjs/icons/components/IconArrowup.js +19 -0
- package/lib/commonjs/icons/components/IconChevrondowncircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronleftcircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronrightcircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronupcircle.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavback.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavcenter.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavhome.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavtask.js +19 -0
- package/lib/commonjs/icons/components/IconSignin.js +19 -0
- package/lib/commonjs/icons/components/IconSignout.js +19 -0
- package/lib/commonjs/icons/components/index.js +132 -0
- package/lib/commonjs/icons/registry.js +2 -2
- package/lib/module/components/AppBar/AppBar.js +70 -6
- package/lib/module/components/AreaLineChart/AreaLineChart.js +859 -0
- package/lib/module/components/AreaLineChart/chartMath.js +242 -0
- package/lib/module/components/Attached/Attached.js +76 -7
- package/lib/module/components/BubbleChart/BubbleChart.js +185 -0
- package/lib/module/components/BubbleChart/bubblePacking.js +370 -0
- package/lib/module/components/Checkbox/Checkbox.js +18 -2
- package/lib/module/components/ClusterBubble/ClusterBubble.js +267 -0
- package/lib/module/components/Drawer/Drawer.js +6 -1
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -6
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
- package/lib/module/components/FormField/FormField.js +3 -16
- package/lib/module/components/FullscreenModal/FullscreenModal.js +5 -1
- package/lib/module/components/ListItem/ListItem.js +6 -11
- package/lib/module/components/MessageField/MessageField.js +3 -15
- package/lib/module/components/MetricLegendItem/MetricLegendItem.js +7 -1
- package/lib/module/components/PaymentFeedback/PaymentFeedback.js +13 -9
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +72 -160
- package/lib/module/components/Spinner/Spinner.js +212 -0
- package/lib/module/components/TextInput/TextInput.js +34 -19
- package/lib/module/components/index.js +4 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/components/IconArrowdown.js +12 -0
- package/lib/module/icons/components/IconArrowup.js +12 -0
- package/lib/module/icons/components/IconChevrondowncircle.js +12 -0
- package/lib/module/icons/components/IconChevronleftcircle.js +12 -0
- package/lib/module/icons/components/IconChevronrightcircle.js +12 -0
- package/lib/module/icons/components/IconChevronupcircle.js +12 -0
- package/lib/module/icons/components/IconOsnavback.js +12 -0
- package/lib/module/icons/components/IconOsnavcenter.js +12 -0
- package/lib/module/icons/components/IconOsnavhome.js +12 -0
- package/lib/module/icons/components/IconOsnavtask.js +12 -0
- package/lib/module/icons/components/IconSignin.js +12 -0
- package/lib/module/icons/components/IconSignout.js +12 -0
- package/lib/module/icons/components/index.js +12 -0
- package/lib/module/icons/registry.js +2 -2
- package/lib/typescript/src/components/AppBar/AppBar.d.ts +12 -1
- package/lib/typescript/src/components/AreaLineChart/AreaLineChart.d.ts +212 -0
- package/lib/typescript/src/components/AreaLineChart/chartMath.d.ts +90 -0
- package/lib/typescript/src/components/Attached/Attached.d.ts +19 -16
- package/lib/typescript/src/components/BubbleChart/BubbleChart.d.ts +81 -0
- package/lib/typescript/src/components/BubbleChart/bubblePacking.d.ts +83 -0
- package/lib/typescript/src/components/ClusterBubble/ClusterBubble.d.ts +76 -0
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +3 -2
- package/lib/typescript/src/components/ListItem/ListItem.d.ts +3 -3
- package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +7 -1
- package/lib/typescript/src/components/PaymentFeedback/PaymentFeedback.d.ts +5 -1
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +10 -8
- package/lib/typescript/src/components/Spinner/Spinner.d.ts +45 -0
- package/lib/typescript/src/components/index.d.ts +4 -0
- package/lib/typescript/src/icons/components/IconArrowdown.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconArrowup.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevrondowncircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronleftcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronrightcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronupcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavback.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavcenter.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavhome.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavtask.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconSignin.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconSignout.d.ts +3 -0
- package/lib/typescript/src/icons/components/index.d.ts +12 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +3 -2
- package/src/components/AppBar/AppBar.tsx +92 -12
- package/src/components/AreaLineChart/AreaLineChart.tsx +1161 -0
- package/src/components/AreaLineChart/chartMath.ts +265 -0
- package/src/components/Attached/Attached.tsx +94 -7
- package/src/components/BubbleChart/BubbleChart.tsx +319 -0
- package/src/components/BubbleChart/bubblePacking.ts +397 -0
- package/src/components/Checkbox/Checkbox.tsx +14 -2
- package/src/components/ClusterBubble/ClusterBubble.tsx +359 -0
- package/src/components/Drawer/Drawer.tsx +4 -0
- package/src/components/DropdownInput/DropdownInput.tsx +54 -20
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +13 -9
- package/src/components/FormField/FormField.tsx +3 -19
- package/src/components/FullscreenModal/FullscreenModal.tsx +3 -0
- package/src/components/ListItem/ListItem.tsx +14 -16
- package/src/components/MessageField/MessageField.tsx +3 -18
- package/src/components/MetricLegendItem/MetricLegendItem.tsx +20 -6
- package/src/components/PaymentFeedback/PaymentFeedback.tsx +15 -8
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +82 -192
- package/src/components/Spinner/Spinner.tsx +273 -0
- package/src/components/TextInput/TextInput.tsx +37 -19
- package/src/components/index.ts +4 -0
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/components/IconArrowdown.tsx +11 -0
- package/src/icons/components/IconArrowup.tsx +11 -0
- package/src/icons/components/IconChevrondowncircle.tsx +11 -0
- package/src/icons/components/IconChevronleftcircle.tsx +11 -0
- package/src/icons/components/IconChevronrightcircle.tsx +11 -0
- package/src/icons/components/IconChevronupcircle.tsx +11 -0
- package/src/icons/components/IconOsnavback.tsx +11 -0
- package/src/icons/components/IconOsnavcenter.tsx +11 -0
- package/src/icons/components/IconOsnavhome.tsx +11 -0
- package/src/icons/components/IconOsnavtask.tsx +11 -0
- package/src/icons/components/IconSignin.tsx +11 -0
- package/src/icons/components/IconSignout.tsx +11 -0
- package/src/icons/components/index.ts +12 -0
- package/src/icons/registry.ts +49 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import React, { useEffect } from 'react'
|
|
2
|
+
import { StyleSheet, View, type StyleProp, type ViewProps, type ViewStyle } from 'react-native'
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
cancelAnimation,
|
|
6
|
+
useAnimatedStyle,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
withRepeat,
|
|
9
|
+
withTiming,
|
|
10
|
+
type SharedValue,
|
|
11
|
+
} from 'react-native-reanimated'
|
|
12
|
+
import Svg, { Path } from 'react-native-svg'
|
|
13
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
14
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
15
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
16
|
+
import { useReducedMotion } from '../../skeleton/useReducedMotion'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Per-segment colours, resolved from the Figma `spiner/*` tokens. Consumers can
|
|
20
|
+
* override any subset via the `colors` prop.
|
|
21
|
+
*/
|
|
22
|
+
export type SpinnerColors = {
|
|
23
|
+
/** Leading segment (front of the falling chain). */
|
|
24
|
+
primary?: string
|
|
25
|
+
/** Middle segment. */
|
|
26
|
+
secondary?: string
|
|
27
|
+
/** Trailing segment (tail of the chain). */
|
|
28
|
+
tertiary?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type SpinnerBaseProps = Omit<ViewProps, 'children' | 'style'>
|
|
32
|
+
|
|
33
|
+
export type SpinnerProps = SpinnerBaseProps & {
|
|
34
|
+
/**
|
|
35
|
+
* Diameter in px. The spinner is always rendered at a 1:1 ratio, so a single
|
|
36
|
+
* size controls both width and height. Defaults to the Figma size (72).
|
|
37
|
+
*/
|
|
38
|
+
size?: number
|
|
39
|
+
/**
|
|
40
|
+
* Duration of one full clockwise revolution of the leading segment, in ms.
|
|
41
|
+
* Lower = faster. Defaults to 2400.
|
|
42
|
+
*/
|
|
43
|
+
durationMs?: number
|
|
44
|
+
/**
|
|
45
|
+
* "Weightiness" of the fall, in `[0, 0.9]`. 0 = perfectly constant speed;
|
|
46
|
+
* higher values make segments whip faster over the top and ease through the
|
|
47
|
+
* bottom. Kept below 1 so the motion never reverses. Defaults to 0.45.
|
|
48
|
+
*/
|
|
49
|
+
gravity?: number
|
|
50
|
+
/** Override any subset of the token-driven segment colours. */
|
|
51
|
+
colors?: SpinnerColors
|
|
52
|
+
/** When false, renders a static resting spinner (also honoured for reduced motion). Defaults to true. */
|
|
53
|
+
animating?: boolean
|
|
54
|
+
/** Design token modes forwarded to token lookups. */
|
|
55
|
+
modes?: Record<string, any>
|
|
56
|
+
/** Container style override. */
|
|
57
|
+
style?: StyleProp<ViewStyle>
|
|
58
|
+
/** Accessibility label announced to assistive tech. Defaults to "Loading". */
|
|
59
|
+
accessibilityLabel?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const SEGMENT_COUNT = 3
|
|
63
|
+
const DEFAULT_SIZE = 72
|
|
64
|
+
const DEFAULT_DURATION_MS = 1500
|
|
65
|
+
const DEFAULT_GRAVITY = 0.45
|
|
66
|
+
|
|
67
|
+
// Stroke thickness as a fraction of the diameter (matches the Figma ring weight).
|
|
68
|
+
const STROKE_RATIO = 0.11
|
|
69
|
+
// Angular length of each individual segment.
|
|
70
|
+
const ARC_LENGTH_DEG = 100
|
|
71
|
+
// Spacing between consecutive heads when fully bunched at the top. Small but
|
|
72
|
+
// non-zero so all three colours stay faintly visible as they crest the top.
|
|
73
|
+
const SPREAD_MIN_DEG = 10
|
|
74
|
+
// Spacing between consecutive heads at full spread. At this extent each segment's
|
|
75
|
+
// tail only overlaps the next head by `ARC_LENGTH_DEG - SPREAD_MAX_DEG` (16°) —
|
|
76
|
+
// the maximum extension the chain reaches while staying connected (never a gap).
|
|
77
|
+
const SPREAD_MAX_DEG = 84
|
|
78
|
+
// Fraction of each revolution spent gradually fanning *out* (the rest is spent
|
|
79
|
+
// snapping back together over the top).
|
|
80
|
+
//
|
|
81
|
+
// This is the knob that balances "reaches full extension" against "never stalls
|
|
82
|
+
// and never recoils". The tail segment's velocity while spreading is
|
|
83
|
+
// `vLead * (1 - spreadRange / (SPREAD_OUT_FRAC * π))`. Spreading the fan-out over
|
|
84
|
+
// ~3/4 of the turn keeps that factor around ~0.45 (so the tail always carries
|
|
85
|
+
// clear forward momentum — it never crawls to a stall, and never reverses),
|
|
86
|
+
// while still letting the breath reach a full 1.0. The remaining ~1/4 is an
|
|
87
|
+
// energetic gather over the top where the trailing segments whip forward to
|
|
88
|
+
// rejoin the lead. A symmetric (sinusoidal/triangle) breath cannot do all three:
|
|
89
|
+
// reach full extension, avoid recoil, and avoid a sustained stall.
|
|
90
|
+
const SPREAD_OUT_FRAC = 0.75
|
|
91
|
+
|
|
92
|
+
const DEG_TO_RAD = Math.PI / 180
|
|
93
|
+
const TWO_PI = Math.PI * 2
|
|
94
|
+
|
|
95
|
+
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value))
|
|
96
|
+
|
|
97
|
+
const toNumber = (value: unknown, fallback: number) => {
|
|
98
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
if (typeof value === 'string') {
|
|
102
|
+
const parsed = Number(value)
|
|
103
|
+
if (Number.isFinite(parsed)) {
|
|
104
|
+
return parsed
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return fallback
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Builds the SVG path for a single fixed-length arc whose *head* sits at the
|
|
112
|
+
* top (12 o'clock) and whose body trails counter-clockwise behind it. Rotating
|
|
113
|
+
* the containing view clockwise then places the head at the desired angle.
|
|
114
|
+
*/
|
|
115
|
+
const buildArcPath = (center: number, radius: number, arcLengthDeg: number) => {
|
|
116
|
+
const arc = arcLengthDeg * DEG_TO_RAD
|
|
117
|
+
// Head at the top: phi = 0 -> (center, center - radius).
|
|
118
|
+
const headX = center
|
|
119
|
+
const headY = center - radius
|
|
120
|
+
// Tail trails counter-clockwise by `arc`: phi = -arc.
|
|
121
|
+
const tailX = center + radius * Math.sin(-arc)
|
|
122
|
+
const tailY = center - radius * Math.cos(-arc)
|
|
123
|
+
const largeArc = arcLengthDeg > 180 ? 1 : 0
|
|
124
|
+
// Sweep from tail -> head is clockwise (sweep flag = 1 in SVG y-down space).
|
|
125
|
+
return `M ${tailX} ${tailY} A ${radius} ${radius} 0 ${largeArc} 1 ${headX} ${headY}`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Animated rotation for one segment.
|
|
130
|
+
*
|
|
131
|
+
* A single linear clock drives a gravity-warped lead angle: it advances faster
|
|
132
|
+
* over the top and slower through the bottom, giving the fall its weight. Each
|
|
133
|
+
* segment trails the lead by `index * offset`, where `offset` breathes between
|
|
134
|
+
* its bunched (top) and spread (bottom) extents in lock-step with the lead's
|
|
135
|
+
* vertical position. Because the offset is bounded by `SPREAD_MAX_DEG`, the
|
|
136
|
+
* three segments form a continuously-overlapping chain that gathers at the top
|
|
137
|
+
* and fans out — fully connected — through the free fall.
|
|
138
|
+
*/
|
|
139
|
+
const useSegmentRotation = (
|
|
140
|
+
clock: SharedValue<number>,
|
|
141
|
+
index: number,
|
|
142
|
+
gravity: number,
|
|
143
|
+
spreadMinRad: number,
|
|
144
|
+
spreadMaxRad: number,
|
|
145
|
+
spreadOutFrac: number,
|
|
146
|
+
) =>
|
|
147
|
+
useAnimatedStyle(() => {
|
|
148
|
+
'worklet'
|
|
149
|
+
const tau = clock.value * TWO_PI
|
|
150
|
+
// Lead angle (clockwise from top). d(lead)/dtau = 1 + gravity*cos(tau) is
|
|
151
|
+
// maximal at the top (tau = 0) and minimal at the bottom (tau = PI), giving
|
|
152
|
+
// the fall its weight.
|
|
153
|
+
const lead = tau + gravity * Math.sin(tau)
|
|
154
|
+
// Breathing is an asymmetric saw in the lead angle: it ramps *gradually* from
|
|
155
|
+
// 0 (bunched, top) up to 1 (fully spread) over `spreadOutFrac` of the turn,
|
|
156
|
+
// then drops back to 0 over the remaining arc (the quick gather over the top).
|
|
157
|
+
// The gentle fan-out slope keeps the trailing segment moving forward at a
|
|
158
|
+
// healthy fraction of the lead's speed — it never stalls and never recoils —
|
|
159
|
+
// while still reaching full extension; the steeper gather is a forward whip,
|
|
160
|
+
// so momentum only ever increases there.
|
|
161
|
+
const leadMod = lead - TWO_PI * Math.floor(lead / TWO_PI)
|
|
162
|
+
const splitLead = spreadOutFrac * TWO_PI
|
|
163
|
+
const breath =
|
|
164
|
+
leadMod < splitLead
|
|
165
|
+
? leadMod / splitLead
|
|
166
|
+
: (TWO_PI - leadMod) / (TWO_PI - splitLead)
|
|
167
|
+
const offset = spreadMinRad + breath * (spreadMaxRad - spreadMinRad)
|
|
168
|
+
const head = lead - index * offset
|
|
169
|
+
return {
|
|
170
|
+
transform: [{ rotate: `${(head * 180) / Math.PI}deg` }],
|
|
171
|
+
}
|
|
172
|
+
}, [gravity, index, spreadMinRad, spreadMaxRad, spreadOutFrac])
|
|
173
|
+
|
|
174
|
+
const fullSize: ViewStyle = { ...StyleSheet.absoluteFillObject }
|
|
175
|
+
|
|
176
|
+
function Spinner({
|
|
177
|
+
size = DEFAULT_SIZE,
|
|
178
|
+
durationMs = DEFAULT_DURATION_MS,
|
|
179
|
+
gravity = DEFAULT_GRAVITY,
|
|
180
|
+
colors,
|
|
181
|
+
animating = true,
|
|
182
|
+
modes: propModes = EMPTY_MODES,
|
|
183
|
+
style,
|
|
184
|
+
accessibilityLabel = 'Loading',
|
|
185
|
+
...rest
|
|
186
|
+
}: SpinnerProps) {
|
|
187
|
+
const { modes: globalModes } = useTokens()
|
|
188
|
+
const modes = { ...globalModes, ...propModes }
|
|
189
|
+
|
|
190
|
+
const systemReducedMotion = useReducedMotion()
|
|
191
|
+
const isAnimated = animating && !systemReducedMotion
|
|
192
|
+
|
|
193
|
+
const resolvedSize = toNumber(size, DEFAULT_SIZE)
|
|
194
|
+
const safeGravity = clamp(toNumber(gravity, DEFAULT_GRAVITY), 0, 0.9)
|
|
195
|
+
const strokeWidth = Math.max(1, resolvedSize * STROKE_RATIO)
|
|
196
|
+
const radius = Math.max(0, (resolvedSize - strokeWidth) / 2)
|
|
197
|
+
const center = resolvedSize / 2
|
|
198
|
+
const arcPath = buildArcPath(center, radius, ARC_LENGTH_DEG)
|
|
199
|
+
|
|
200
|
+
const segmentColors = [
|
|
201
|
+
colors?.primary ?? (getVariableByName('spiner/primary/bg', modes) as string) ?? '#d0a259',
|
|
202
|
+
colors?.secondary ?? (getVariableByName('spiner/secondary/bg', modes) as string) ?? '#5b00b5',
|
|
203
|
+
colors?.tertiary ?? (getVariableByName('spiner/tertiary/bg', modes) as string) ?? '#066b99',
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
const clock = useSharedValue(0)
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!isAnimated) {
|
|
210
|
+
cancelAnimation(clock)
|
|
211
|
+
clock.value = 0
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
clock.value = 0
|
|
215
|
+
clock.value = withRepeat(
|
|
216
|
+
withTiming(1, { duration: Math.max(1, durationMs), easing: Easing.linear }),
|
|
217
|
+
-1,
|
|
218
|
+
false,
|
|
219
|
+
)
|
|
220
|
+
return () => {
|
|
221
|
+
cancelAnimation(clock)
|
|
222
|
+
}
|
|
223
|
+
}, [isAnimated, durationMs, clock])
|
|
224
|
+
|
|
225
|
+
// Hooks must run unconditionally and in a stable order, so all three segment
|
|
226
|
+
// styles are always computed even when the spinner renders statically.
|
|
227
|
+
const spreadMinRad = SPREAD_MIN_DEG * DEG_TO_RAD
|
|
228
|
+
const spreadMaxRad = SPREAD_MAX_DEG * DEG_TO_RAD
|
|
229
|
+
const style0 = useSegmentRotation(clock, 0, safeGravity, spreadMinRad, spreadMaxRad, SPREAD_OUT_FRAC)
|
|
230
|
+
const style1 = useSegmentRotation(clock, 1, safeGravity, spreadMinRad, spreadMaxRad, SPREAD_OUT_FRAC)
|
|
231
|
+
const style2 = useSegmentRotation(clock, 2, safeGravity, spreadMinRad, spreadMaxRad, SPREAD_OUT_FRAC)
|
|
232
|
+
const animatedStyles = [style0, style1, style2]
|
|
233
|
+
|
|
234
|
+
// Static resting fan (evenly spaced) used when animation is disabled.
|
|
235
|
+
const restingRotations = [0, -120, -240]
|
|
236
|
+
|
|
237
|
+
const containerStyle: ViewStyle = {
|
|
238
|
+
height: resolvedSize,
|
|
239
|
+
width: resolvedSize,
|
|
240
|
+
position: 'relative',
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<View
|
|
245
|
+
accessibilityRole="progressbar"
|
|
246
|
+
accessibilityLabel={accessibilityLabel}
|
|
247
|
+
style={[containerStyle, style]}
|
|
248
|
+
{...rest}
|
|
249
|
+
>
|
|
250
|
+
{/* Render tail -> head so the leading segment overlaps on top. */}
|
|
251
|
+
{Array.from({ length: SEGMENT_COUNT }, (_, i) => SEGMENT_COUNT - 1 - i).map((segmentIndex) => {
|
|
252
|
+
const segmentStyle = isAnimated
|
|
253
|
+
? animatedStyles[segmentIndex]
|
|
254
|
+
: { transform: [{ rotate: `${restingRotations[segmentIndex]}deg` }] }
|
|
255
|
+
return (
|
|
256
|
+
<Animated.View key={segmentIndex} style={[fullSize, segmentStyle]} pointerEvents="none">
|
|
257
|
+
<Svg width={resolvedSize} height={resolvedSize} viewBox={`0 0 ${resolvedSize} ${resolvedSize}`}>
|
|
258
|
+
<Path
|
|
259
|
+
d={arcPath}
|
|
260
|
+
stroke={segmentColors[segmentIndex]}
|
|
261
|
+
strokeWidth={strokeWidth}
|
|
262
|
+
strokeLinecap="round"
|
|
263
|
+
fill="none"
|
|
264
|
+
/>
|
|
265
|
+
</Svg>
|
|
266
|
+
</Animated.View>
|
|
267
|
+
)
|
|
268
|
+
})}
|
|
269
|
+
</View>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export default Spinner
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useRef, useState } from 'react'
|
|
2
|
-
import { Pressable, View, TextInput as RNTextInput, type StyleProp, type ViewStyle, type TextInputProps as RNTextInputProps, type TextStyle } from 'react-native'
|
|
2
|
+
import { Platform, Pressable, View, TextInput as RNTextInput, type StyleProp, type ViewStyle, type TextInputProps as RNTextInputProps, type TextStyle } from 'react-native'
|
|
3
3
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
4
|
import Icon from '../../icons/Icon'
|
|
5
5
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
@@ -36,6 +36,8 @@ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
|
36
36
|
* Helper function to convert a color to a more transparent version for placeholder text.
|
|
37
37
|
* Takes a color string (hex, rgb, rgba) and returns it with reduced opacity.
|
|
38
38
|
*/
|
|
39
|
+
const IS_WEB = Platform.OS === 'web'
|
|
40
|
+
|
|
39
41
|
function makePlaceholderColor(color: string | undefined, opacity: number = 0.5): string {
|
|
40
42
|
if (!color || typeof color !== 'string') {
|
|
41
43
|
return color || ''
|
|
@@ -128,10 +130,9 @@ function TextInput({
|
|
|
128
130
|
// Track focus state to hide placeholder when focused
|
|
129
131
|
const [isFocused, setIsFocused] = useState(false)
|
|
130
132
|
const [isHovered, setIsHovered] = useState(false)
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// native input only gains focus on the *second* tap.
|
|
133
|
+
// On web we keep a ref so a click anywhere inside the (Pressable) wrapper can
|
|
134
|
+
// focus the input. On native the wrapper is a plain View and the native
|
|
135
|
+
// input focuses itself on the first tap (see container note below).
|
|
135
136
|
const inputRef = useRef<RNTextInput>(null)
|
|
136
137
|
|
|
137
138
|
// Resolve container tokens
|
|
@@ -227,19 +228,19 @@ function TextInput({
|
|
|
227
228
|
// Generate default accessibility label from placeholder if not provided
|
|
228
229
|
const defaultAccessibilityLabel = accessibilityLabel || (placeholder || 'Text input')
|
|
229
230
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
231
|
+
// IMPORTANT (Android focus reliability):
|
|
232
|
+
// Do NOT wrap the native <TextInput> in a Pressable/Touchable on native.
|
|
233
|
+
// A touch-responder-claiming wrapper steals the first tap, which is the
|
|
234
|
+
// classic cause of the "needs 2–3 taps to focus" Android bug — and forwarding
|
|
235
|
+
// focus from `onPress` is unreliable because the press is cancelled by the
|
|
236
|
+
// tiniest finger movement. A plain <View> does not claim the responder, so
|
|
237
|
+
// the native input receives the tap and focuses on the FIRST tap.
|
|
238
|
+
// On web there is no such issue, so we keep the Pressable for the hover
|
|
239
|
+
// affordance plus click-anywhere-to-focus.
|
|
240
|
+
const containerStyleArray = [containerStyle, focusContainerStyle, hoverStyle, style]
|
|
241
|
+
|
|
242
|
+
const inner = (
|
|
243
|
+
<>
|
|
243
244
|
{processedLeading && (
|
|
244
245
|
<View
|
|
245
246
|
accessibilityElementsHidden={true}
|
|
@@ -269,8 +270,25 @@ function TextInput({
|
|
|
269
270
|
{processedTrailing}
|
|
270
271
|
</View>
|
|
271
272
|
)}
|
|
272
|
-
|
|
273
|
+
</>
|
|
273
274
|
)
|
|
275
|
+
|
|
276
|
+
if (IS_WEB) {
|
|
277
|
+
return (
|
|
278
|
+
<Pressable
|
|
279
|
+
style={containerStyleArray}
|
|
280
|
+
onHoverIn={() => setIsHovered(true)}
|
|
281
|
+
onHoverOut={() => setIsHovered(false)}
|
|
282
|
+
// Web: clicking the padding / icon gutter focuses the input too.
|
|
283
|
+
onPress={() => inputRef.current?.focus()}
|
|
284
|
+
accessible={false}
|
|
285
|
+
>
|
|
286
|
+
{inner}
|
|
287
|
+
</Pressable>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return <View style={containerStyleArray}>{inner}</View>
|
|
274
292
|
}
|
|
275
293
|
|
|
276
294
|
/**
|
package/src/components/index.ts
CHANGED
|
@@ -65,6 +65,7 @@ export { default as Title, type TitleProps } from './Title/Title';
|
|
|
65
65
|
export { default as Screen, type ScreenProps } from './Screen/Screen';
|
|
66
66
|
export { default as Section } from './Section/Section';
|
|
67
67
|
export { default as Slot, type SlotProps, type SlotLayoutDirection } from './Slot/Slot';
|
|
68
|
+
export { default as Spinner, type SpinnerProps, type SpinnerColors } from './Spinner/Spinner';
|
|
68
69
|
export { default as Stepper, type StepperProps } from './Stepper/Stepper';
|
|
69
70
|
export { Step, type StepProps, type StepStatus } from './Stepper/Step';
|
|
70
71
|
export { StepLabel, type StepLabelProps } from './Stepper/StepLabel';
|
|
@@ -109,6 +110,9 @@ export { default as RadioButton, type RadioButtonProps } from './RadioButton/Rad
|
|
|
109
110
|
export { default as RechargeCard, type RechargeCardProps } from './RechargeCard/RechargeCard';
|
|
110
111
|
export { default as SavingsGoalSummary, type SavingsGoalSummaryProps, type SavingsGoalSummaryItem } from './SavingsGoalSummary/SavingsGoalSummary';
|
|
111
112
|
export { default as DonutChart, type DonutChartProps, type DonutChartSegmentData, type DonutChartSegmentProps, DonutChartSegment } from './DonutChart/DonutChart';
|
|
113
|
+
export { default as AreaLineChart, useChart, type AreaLineChartProps, type ChartSeries, type ChartPoint, type ChartInset, type GoalPinConfig, type ChartGridProps, type ChartXAxisProps, type ChartYAxisProps, type ChartGoalPinProps } from './AreaLineChart/AreaLineChart';
|
|
114
|
+
export { default as ClusterBubble, type ClusterBubbleProps, type ClusterBubbleLabelPlacement, type ClusterBubbleLabelDirection } from './ClusterBubble/ClusterBubble';
|
|
115
|
+
export { default as BubbleChart, type BubbleChartProps, type BubbleDatum } from './BubbleChart/BubbleChart';
|
|
112
116
|
export { default as DonutChartSummary, type DonutChartSummaryProps, type DonutChartSummaryItem } from './DonutChartSummary/DonutChartSummary';
|
|
113
117
|
export { default as RangeTrack, type RangeTrackProps, type RangeTrackTab, type RangeTrackItem } from './RangeTrack/RangeTrack';
|
|
114
118
|
export { default as SegmentedTrack, type SegmentedTrackProps, type SegmentedTrackSegmentData, type SegmentedTrackSegmentProps, SegmentedTrackSegment } from './SegmentedTrack/SegmentedTrack';
|