panelui-native 0.103.2 → 0.105.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 +2 -0
- package/lib/module/components/countdown/countdown-time.js +96 -0
- package/lib/module/components/countdown/countdown-time.js.map +1 -0
- package/lib/module/components/countdown/index.js +345 -0
- package/lib/module/components/countdown/index.js.map +1 -0
- package/lib/module/components/flow/index.js +64 -55
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/leaderboard/index.js +442 -0
- package/lib/module/components/leaderboard/index.js.map +1 -0
- package/lib/module/components/leaderboard/leaderboard-rank.js +65 -0
- package/lib/module/components/leaderboard/leaderboard-rank.js.map +1 -0
- package/lib/module/components/map/index.js +114 -50
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/countdown/countdown-time.d.ts +53 -0
- package/lib/typescript/src/components/countdown/countdown-time.d.ts.map +1 -0
- package/lib/typescript/src/components/countdown/index.d.ts +170 -0
- package/lib/typescript/src/components/countdown/index.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/leaderboard/index.d.ts +63 -0
- package/lib/typescript/src/components/leaderboard/index.d.ts.map +1 -0
- package/lib/typescript/src/components/leaderboard/leaderboard-rank.d.ts +41 -0
- package/lib/typescript/src/components/leaderboard/leaderboard-rank.d.ts.map +1 -0
- package/lib/typescript/src/components/map/index.d.ts +30 -5
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/countdown/countdown-time.ts +105 -0
- package/src/components/countdown/index.tsx +335 -0
- package/src/components/flow/index.tsx +65 -51
- package/src/components/leaderboard/index.tsx +535 -0
- package/src/components/leaderboard/leaderboard-rank.ts +84 -0
- package/src/components/map/index.tsx +130 -44
- package/src/index.ts +15 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time arithmetic for `Countdown`, kept apart from the views so it can be
|
|
3
|
+
* tested without a clock.
|
|
4
|
+
*
|
|
5
|
+
* Whole seconds are rounded **up**. A countdown that floors shows 0:00 for the
|
|
6
|
+
* last second before it ends, so it reads as finished while it is not — and
|
|
7
|
+
* an "ends at 12:00" sale still taking orders under a zero is the bug people
|
|
8
|
+
* report. Rounding up shows 0:01 until the very end and reaches 0:00 at the
|
|
9
|
+
* moment the target passes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type CountdownUnit = 'days' | 'hours' | 'minutes' | 'seconds';
|
|
13
|
+
|
|
14
|
+
export const COUNTDOWN_UNITS: readonly CountdownUnit[] = ['days', 'hours', 'minutes', 'seconds'];
|
|
15
|
+
|
|
16
|
+
const SECONDS: Record<CountdownUnit, number> = {
|
|
17
|
+
days: 86400,
|
|
18
|
+
hours: 3600,
|
|
19
|
+
minutes: 60,
|
|
20
|
+
seconds: 1,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface CountdownPart {
|
|
24
|
+
unit: CountdownUnit;
|
|
25
|
+
value: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Whole seconds left until `target`, rounded up, never below zero. */
|
|
29
|
+
export function secondsLeft(target: number, now: number): number {
|
|
30
|
+
if (!Number.isFinite(target) || !Number.isFinite(now)) return 0;
|
|
31
|
+
return Math.max(0, Math.ceil((target - now) / 1000));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Milliseconds until the displayed second next changes.
|
|
36
|
+
*
|
|
37
|
+
* The tick is scheduled for that moment rather than every 1000ms from mount,
|
|
38
|
+
* so the display changes when the second actually turns over instead of
|
|
39
|
+
* drifting by however late the first timer fired.
|
|
40
|
+
*/
|
|
41
|
+
export function msUntilNextTick(target: number, now: number): number {
|
|
42
|
+
const remaining = target - now;
|
|
43
|
+
if (remaining <= 0) return 0;
|
|
44
|
+
const into = remaining % 1000;
|
|
45
|
+
return into === 0 ? 1000 : into;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Splits a number of seconds across the given units, largest first.
|
|
50
|
+
*
|
|
51
|
+
* The largest unit absorbs everything above it, so with `['hours',
|
|
52
|
+
* 'minutes', 'seconds']` two days reads as 48 hours rather than losing the
|
|
53
|
+
* days. Units are always returned in largest-to-smallest order whatever order
|
|
54
|
+
* they were passed in.
|
|
55
|
+
*/
|
|
56
|
+
export function splitSeconds(
|
|
57
|
+
total: number,
|
|
58
|
+
units: readonly CountdownUnit[] = COUNTDOWN_UNITS
|
|
59
|
+
): CountdownPart[] {
|
|
60
|
+
const wanted = COUNTDOWN_UNITS.filter((unit) => units.includes(unit));
|
|
61
|
+
if (wanted.length === 0) return [];
|
|
62
|
+
let rest = Math.max(0, Math.floor(total));
|
|
63
|
+
return wanted.map((unit) => {
|
|
64
|
+
const value = Math.floor(rest / SECONDS[unit]);
|
|
65
|
+
rest -= value * SECONDS[unit];
|
|
66
|
+
return { unit, value };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Drops leading units that are zero, keeping at least `keep` of them.
|
|
72
|
+
*
|
|
73
|
+
* A launch three hours away has no use for a "00 days" box, but a countdown
|
|
74
|
+
* should not shrink to a lone seconds field in its last minute either, so the
|
|
75
|
+
* smallest `keep` units always stay.
|
|
76
|
+
*/
|
|
77
|
+
export function trimLeading(parts: CountdownPart[], keep = 2): CountdownPart[] {
|
|
78
|
+
let start = 0;
|
|
79
|
+
while (start < parts.length - keep && parts[start]!.value === 0) start += 1;
|
|
80
|
+
return parts.slice(start);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const NAMES: Record<CountdownUnit, [string, string]> = {
|
|
84
|
+
days: ['day', 'days'],
|
|
85
|
+
hours: ['hour', 'hours'],
|
|
86
|
+
minutes: ['minute', 'minutes'],
|
|
87
|
+
seconds: ['second', 'seconds'],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* What a screen reader hears. Coarser than what is on screen.
|
|
92
|
+
*
|
|
93
|
+
* The label changes only when the minute does, until the last minute, when it
|
|
94
|
+
* counts seconds. A label that changed every second would be re-read every
|
|
95
|
+
* second by anything polling it, and a reader does not need the seconds of a
|
|
96
|
+
* launch two days away.
|
|
97
|
+
*/
|
|
98
|
+
export function describeSeconds(total: number): string {
|
|
99
|
+
if (total <= 0) return 'Time is up';
|
|
100
|
+
if (total < 60) return `${total} ${NAMES.seconds[total === 1 ? 0 : 1]} left`;
|
|
101
|
+
const parts = splitSeconds(total, ['days', 'hours', 'minutes'])
|
|
102
|
+
.filter((part) => part.value > 0)
|
|
103
|
+
.map((part) => `${part.value} ${NAMES[part.unit][part.value === 1 ? 0 : 1]}`);
|
|
104
|
+
return `${parts.join(', ')} left`;
|
|
105
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Countdown — the time left until a moment, ticking.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <Countdown to={launch} />
|
|
6
|
+
* <Countdown to={saleEnds} variant="inline" urgentBelow={300} />
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* The clock is read, not counted. Every tick works out what is left from
|
|
10
|
+
* `Date.now()` and the target, and the next tick is scheduled for the moment
|
|
11
|
+
* the displayed second turns over. A countdown that subtracted one per
|
|
12
|
+
* interval would drift by however late each timer fired, and would be wrong
|
|
13
|
+
* by the whole time the app spent in the background.
|
|
14
|
+
*
|
|
15
|
+
* Each digit is its own keyed view: when it changes, the old one slides out
|
|
16
|
+
* downward and the new one drops in from above, on the UI thread. Only the
|
|
17
|
+
* digits that changed move, so a ticking clock is one column moving, not the
|
|
18
|
+
* whole number re-rendering. Nothing moves on the first render, and nothing
|
|
19
|
+
* moves at all under reduce motion.
|
|
20
|
+
*
|
|
21
|
+
* The screen-reader label is coarser than the display — minutes until the
|
|
22
|
+
* last minute, then seconds — because a label that changed every second
|
|
23
|
+
* would be re-read every second. See `countdown-time.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
26
|
+
import { View, type ViewProps } from 'react-native';
|
|
27
|
+
import Animated, {
|
|
28
|
+
Easing,
|
|
29
|
+
useReducedMotion,
|
|
30
|
+
withTiming,
|
|
31
|
+
type EntryAnimationsValues,
|
|
32
|
+
type ExitAnimationsValues,
|
|
33
|
+
} from 'react-native-reanimated';
|
|
34
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
35
|
+
import { Text } from '../../primitives/text';
|
|
36
|
+
import { cn } from '../../utils/cn';
|
|
37
|
+
import {
|
|
38
|
+
describeSeconds,
|
|
39
|
+
msUntilNextTick,
|
|
40
|
+
secondsLeft,
|
|
41
|
+
splitSeconds,
|
|
42
|
+
trimLeading,
|
|
43
|
+
type CountdownPart,
|
|
44
|
+
type CountdownUnit,
|
|
45
|
+
} from './countdown-time';
|
|
46
|
+
|
|
47
|
+
export type { CountdownUnit } from './countdown-time';
|
|
48
|
+
|
|
49
|
+
/** How long a digit takes to change. Well inside the second it is shown for. */
|
|
50
|
+
const DIGIT_MS = 260;
|
|
51
|
+
const EASE = Easing.out(Easing.cubic);
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* The new digit drops into its slot from one digit-height above while the old
|
|
55
|
+
* one drops out by one digit-height below, so the two travel together like an
|
|
56
|
+
* odometer wheel and are never on top of each other. A fixed distance would
|
|
57
|
+
* overlap them at large sizes and overshoot the window at small ones, so the
|
|
58
|
+
* distance is the measured height of the digit itself.
|
|
59
|
+
*/
|
|
60
|
+
function enter(values: EntryAnimationsValues) {
|
|
61
|
+
'worklet';
|
|
62
|
+
return {
|
|
63
|
+
initialValues: { originY: values.targetOriginY - values.targetHeight, opacity: 0 },
|
|
64
|
+
animations: {
|
|
65
|
+
originY: withTiming(values.targetOriginY, { duration: DIGIT_MS, easing: EASE }),
|
|
66
|
+
opacity: withTiming(1, { duration: DIGIT_MS, easing: EASE }),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function exit(values: ExitAnimationsValues) {
|
|
72
|
+
'worklet';
|
|
73
|
+
return {
|
|
74
|
+
initialValues: { originY: values.currentOriginY, opacity: 1 },
|
|
75
|
+
animations: {
|
|
76
|
+
originY: withTiming(values.currentOriginY + values.currentHeight, {
|
|
77
|
+
duration: DIGIT_MS,
|
|
78
|
+
easing: EASE,
|
|
79
|
+
}),
|
|
80
|
+
opacity: withTiming(0, { duration: DIGIT_MS, easing: EASE }),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const countdownVariants = tv({
|
|
86
|
+
slots: {
|
|
87
|
+
root: 'flex-row items-start',
|
|
88
|
+
segment: 'items-center',
|
|
89
|
+
digits: 'flex-row',
|
|
90
|
+
digit: 'font-semibold tabular-nums text-foreground',
|
|
91
|
+
label: 'font-medium uppercase tracking-wider text-muted-foreground',
|
|
92
|
+
separator: 'font-semibold tabular-nums text-muted-foreground',
|
|
93
|
+
suffix: 'font-semibold text-muted-foreground',
|
|
94
|
+
},
|
|
95
|
+
variants: {
|
|
96
|
+
/**
|
|
97
|
+
* `segmented` puts each unit in its own box with its name under it — a
|
|
98
|
+
* launch page, a hero, anything the countdown is the point of.
|
|
99
|
+
* `inline` is a line of text, `2d 14:03:22`, for a banner, a button
|
|
100
|
+
* caption or a row.
|
|
101
|
+
*/
|
|
102
|
+
variant: {
|
|
103
|
+
segmented: {
|
|
104
|
+
root: 'gap-2',
|
|
105
|
+
segment: 'gap-1.5 rounded-xl border border-border bg-card shadow-sm',
|
|
106
|
+
},
|
|
107
|
+
inline: {
|
|
108
|
+
root: 'items-baseline',
|
|
109
|
+
segment: 'flex-row items-baseline',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
size: {
|
|
113
|
+
sm: { digit: 'text-lg', label: 'text-[10px]', separator: 'text-lg', suffix: 'text-sm' },
|
|
114
|
+
md: { digit: 'text-3xl', label: 'text-[10px]', separator: 'text-3xl', suffix: 'text-xl' },
|
|
115
|
+
lg: { digit: 'text-4xl', label: 'text-[11px]', separator: 'text-4xl', suffix: 'text-2xl' },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
compoundVariants: [
|
|
119
|
+
{ variant: 'segmented', size: 'sm', class: { segment: 'min-w-12 px-2 py-1.5' } },
|
|
120
|
+
{ variant: 'segmented', size: 'md', class: { segment: 'min-w-16 px-3 py-2.5' } },
|
|
121
|
+
{ variant: 'segmented', size: 'lg', class: { segment: 'min-w-16 px-3 py-3' } },
|
|
122
|
+
],
|
|
123
|
+
defaultVariants: { variant: 'segmented', size: 'md' },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
type CountdownVariantProps = VariantProps<typeof countdownVariants>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The urgent colour. A class added on top rather than a `tv()` variant: it is
|
|
130
|
+
* derived from `urgentBelow` and the clock, not passed, and a variant here
|
|
131
|
+
* would be documented as a prop.
|
|
132
|
+
*/
|
|
133
|
+
const URGENT = { digit: 'text-destructive', separator: 'text-destructive/60' };
|
|
134
|
+
|
|
135
|
+
const DEFAULT_LABELS: Record<CountdownUnit, string> = {
|
|
136
|
+
days: 'Days',
|
|
137
|
+
hours: 'Hours',
|
|
138
|
+
minutes: 'Minutes',
|
|
139
|
+
seconds: 'Seconds',
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const SUFFIX: Record<CountdownUnit, string> = { days: 'd', hours: 'h', minutes: 'm', seconds: 's' };
|
|
143
|
+
|
|
144
|
+
export interface CountdownProps extends Omit<ViewProps, 'children'>, CountdownVariantProps {
|
|
145
|
+
className?: string;
|
|
146
|
+
/** The moment to count down to, as a `Date` or epoch milliseconds. */
|
|
147
|
+
to: Date | number;
|
|
148
|
+
/**
|
|
149
|
+
* Which units to show, largest to smallest. The largest one absorbs
|
|
150
|
+
* everything above it — `['hours', 'minutes', 'seconds']` shows two days as
|
|
151
|
+
* 48 hours. Defaults to all four.
|
|
152
|
+
*/
|
|
153
|
+
units?: CountdownUnit[];
|
|
154
|
+
/**
|
|
155
|
+
* Drop leading units while they are zero, so a launch three hours away has
|
|
156
|
+
* no "00 days" box. The smallest two always stay.
|
|
157
|
+
*/
|
|
158
|
+
trim?: boolean;
|
|
159
|
+
/** Names under each box in `segmented`. Pass any subset to translate them. */
|
|
160
|
+
labels?: Partial<Record<CountdownUnit, string>>;
|
|
161
|
+
/**
|
|
162
|
+
* Turn the digits to the destructive colour once this many seconds or fewer
|
|
163
|
+
* are left — the last five minutes of a sale, the last ten seconds of a bid.
|
|
164
|
+
*/
|
|
165
|
+
urgentBelow?: number;
|
|
166
|
+
/** Called once when the time is up, including on mount if it already is. */
|
|
167
|
+
onComplete?: () => void;
|
|
168
|
+
/** Called with the whole seconds left each time the display changes. */
|
|
169
|
+
onTick?: (secondsLeft: number) => void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** One digit in a window its own height, swapped with a slide when it changes. */
|
|
173
|
+
function Digit({
|
|
174
|
+
value,
|
|
175
|
+
animate,
|
|
176
|
+
className,
|
|
177
|
+
}: {
|
|
178
|
+
value: string;
|
|
179
|
+
animate: boolean;
|
|
180
|
+
className: string;
|
|
181
|
+
}) {
|
|
182
|
+
return (
|
|
183
|
+
<View className="overflow-hidden">
|
|
184
|
+
{/* In flow and invisible: it gives the window a digit's width and height. */}
|
|
185
|
+
<Text className={cn(className, 'opacity-0')}>0</Text>
|
|
186
|
+
<Animated.View
|
|
187
|
+
key={value}
|
|
188
|
+
entering={animate ? enter : undefined}
|
|
189
|
+
exiting={animate ? exit : undefined}
|
|
190
|
+
style={{ position: 'absolute', top: 0, left: 0, right: 0 }}
|
|
191
|
+
>
|
|
192
|
+
<Text className={cn(className, 'text-center')}>{value}</Text>
|
|
193
|
+
</Animated.View>
|
|
194
|
+
</View>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function Digits({
|
|
199
|
+
part,
|
|
200
|
+
animate,
|
|
201
|
+
className,
|
|
202
|
+
}: {
|
|
203
|
+
part: CountdownPart;
|
|
204
|
+
animate: boolean;
|
|
205
|
+
className: string;
|
|
206
|
+
}) {
|
|
207
|
+
const slots = countdownVariants();
|
|
208
|
+
const text = String(part.value).padStart(2, '0');
|
|
209
|
+
return (
|
|
210
|
+
<View className={slots.digits()}>
|
|
211
|
+
{text.split('').map((char, index) => (
|
|
212
|
+
// Keyed by place from the right, so a days count going from 10 to 9
|
|
213
|
+
// keeps the ones column it already had.
|
|
214
|
+
<Digit
|
|
215
|
+
key={text.length - index}
|
|
216
|
+
value={char}
|
|
217
|
+
animate={animate}
|
|
218
|
+
className={className}
|
|
219
|
+
/>
|
|
220
|
+
))}
|
|
221
|
+
</View>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const CountdownRoot = forwardRef<View, CountdownProps>(function Countdown(
|
|
226
|
+
{
|
|
227
|
+
className,
|
|
228
|
+
to,
|
|
229
|
+
variant = 'segmented',
|
|
230
|
+
size = 'md',
|
|
231
|
+
units,
|
|
232
|
+
trim = true,
|
|
233
|
+
labels,
|
|
234
|
+
urgentBelow,
|
|
235
|
+
onComplete,
|
|
236
|
+
onTick,
|
|
237
|
+
...props
|
|
238
|
+
},
|
|
239
|
+
ref
|
|
240
|
+
) {
|
|
241
|
+
const target = typeof to === 'number' ? to : to.getTime();
|
|
242
|
+
const [left, setLeft] = useState(() => secondsLeft(target, Date.now()));
|
|
243
|
+
|
|
244
|
+
// Callbacks through refs, so a new arrow function from the parent on every
|
|
245
|
+
// render does not restart the clock.
|
|
246
|
+
const completeRef = useRef(onComplete);
|
|
247
|
+
const tickRef = useRef(onTick);
|
|
248
|
+
completeRef.current = onComplete;
|
|
249
|
+
tickRef.current = onTick;
|
|
250
|
+
|
|
251
|
+
// No animation on the first render: the numbers are arriving, not changing.
|
|
252
|
+
const mounted = useRef(false);
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
mounted.current = true;
|
|
255
|
+
}, []);
|
|
256
|
+
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
259
|
+
let done = false;
|
|
260
|
+
const tick = () => {
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
const next = secondsLeft(target, now);
|
|
263
|
+
setLeft(next);
|
|
264
|
+
tickRef.current?.(next);
|
|
265
|
+
if (next === 0) {
|
|
266
|
+
if (!done) {
|
|
267
|
+
done = true;
|
|
268
|
+
completeRef.current?.();
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// A few milliseconds past the turn, so the read lands in the new second.
|
|
273
|
+
timer = setTimeout(tick, msUntilNextTick(target, now) + 8);
|
|
274
|
+
};
|
|
275
|
+
tick();
|
|
276
|
+
return () => clearTimeout(timer);
|
|
277
|
+
}, [target]);
|
|
278
|
+
|
|
279
|
+
const urgent = urgentBelow != null && left > 0 && left <= urgentBelow;
|
|
280
|
+
const variants = countdownVariants({ variant, size });
|
|
281
|
+
const slots = {
|
|
282
|
+
...variants,
|
|
283
|
+
digit: () => cn(variants.digit(), urgent && URGENT.digit),
|
|
284
|
+
separator: () => cn(variants.separator(), urgent && URGENT.separator),
|
|
285
|
+
};
|
|
286
|
+
const split = splitSeconds(left, units);
|
|
287
|
+
const parts = trim ? trimLeading(split) : split;
|
|
288
|
+
// Custom layout animations do not read the system setting on their own.
|
|
289
|
+
const reduced = useReducedMotion();
|
|
290
|
+
const animate = mounted.current && !reduced;
|
|
291
|
+
const names = { ...DEFAULT_LABELS, ...labels };
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<View
|
|
295
|
+
ref={ref}
|
|
296
|
+
accessible
|
|
297
|
+
accessibilityRole="timer"
|
|
298
|
+
accessibilityLabel={describeSeconds(left)}
|
|
299
|
+
className={slots.root({ className })}
|
|
300
|
+
{...props}
|
|
301
|
+
>
|
|
302
|
+
{parts.map((part, index) => {
|
|
303
|
+
if (variant === 'inline') {
|
|
304
|
+
// Days read as "2d" ahead of a clock; the rest as hh:mm:ss.
|
|
305
|
+
const isDays = part.unit === 'days';
|
|
306
|
+
const nextIsClock = parts[index + 1] && parts[index + 1]!.unit !== 'days';
|
|
307
|
+
return (
|
|
308
|
+
<View key={part.unit} className={slots.segment()}>
|
|
309
|
+
{isDays ? (
|
|
310
|
+
<>
|
|
311
|
+
<Text className={slots.digit()}>{part.value}</Text>
|
|
312
|
+
<Text className={slots.suffix()}>{SUFFIX.days} </Text>
|
|
313
|
+
</>
|
|
314
|
+
) : (
|
|
315
|
+
<>
|
|
316
|
+
<Digits part={part} animate={animate} className={slots.digit()} />
|
|
317
|
+
{nextIsClock ? <Text className={slots.separator()}>:</Text> : null}
|
|
318
|
+
</>
|
|
319
|
+
)}
|
|
320
|
+
</View>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<View key={part.unit} className={slots.segment()}>
|
|
326
|
+
<Digits part={part} animate={animate} className={slots.digit()} />
|
|
327
|
+
<Text className={slots.label()}>{names[part.unit]}</Text>
|
|
328
|
+
</View>
|
|
329
|
+
);
|
|
330
|
+
})}
|
|
331
|
+
</View>
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
export const Countdown = CountdownRoot;
|
|
@@ -89,7 +89,14 @@ import Svg, {
|
|
|
89
89
|
Rect,
|
|
90
90
|
} from 'react-native-svg';
|
|
91
91
|
import { tv } from 'tailwind-variants';
|
|
92
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
IconColorProvider,
|
|
94
|
+
LockIcon,
|
|
95
|
+
MaximizeIcon,
|
|
96
|
+
MinusIcon,
|
|
97
|
+
PlusIcon,
|
|
98
|
+
UnlockIcon,
|
|
99
|
+
} from '../../icons';
|
|
93
100
|
import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
94
101
|
import { Text, textChildren } from '../../primitives/text';
|
|
95
102
|
import { cn } from '../../utils/cn';
|
|
@@ -2027,58 +2034,65 @@ function FlowControls({
|
|
|
2027
2034
|
}: FlowControlsProps) {
|
|
2028
2035
|
const { zoomBy, fitView, locked, setLocked } = useFlow('Flow.Controls');
|
|
2029
2036
|
const slots = flowVariants();
|
|
2037
|
+
// One colour for every glyph in the stack. Without it each icon falls back
|
|
2038
|
+
// to its own default, and the minus sign's is white — drawn for a checked
|
|
2039
|
+
// checkbox, and invisible on the card.
|
|
2040
|
+
const token = useCSSVariable('--color-muted-foreground');
|
|
2041
|
+
const tint = typeof token === 'string' ? token : '#737373';
|
|
2030
2042
|
|
|
2031
2043
|
return (
|
|
2032
|
-
<
|
|
2033
|
-
{
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2044
|
+
<IconColorProvider color={tint}>
|
|
2045
|
+
<View className={slots.controls({ className })}>
|
|
2046
|
+
{showZoom ? (
|
|
2047
|
+
<>
|
|
2048
|
+
<AnimatedPressable
|
|
2049
|
+
accessibilityRole="button"
|
|
2050
|
+
accessibilityLabel="Zoom in"
|
|
2051
|
+
className={slots.control()}
|
|
2052
|
+
onPress={() => zoomBy(step)}
|
|
2053
|
+
>
|
|
2054
|
+
<PlusIcon size={16} />
|
|
2055
|
+
</AnimatedPressable>
|
|
2056
|
+
<View className="h-px bg-border" />
|
|
2057
|
+
<AnimatedPressable
|
|
2058
|
+
accessibilityRole="button"
|
|
2059
|
+
accessibilityLabel="Zoom out"
|
|
2060
|
+
className={slots.control()}
|
|
2061
|
+
onPress={() => zoomBy(1 / step)}
|
|
2062
|
+
>
|
|
2063
|
+
<MinusIcon size={16} strokeWidth={2} />
|
|
2064
|
+
</AnimatedPressable>
|
|
2065
|
+
</>
|
|
2066
|
+
) : null}
|
|
2067
|
+
{fit ? (
|
|
2068
|
+
<>
|
|
2069
|
+
<View className="h-px bg-border" />
|
|
2070
|
+
<AnimatedPressable
|
|
2071
|
+
accessibilityRole="button"
|
|
2072
|
+
accessibilityLabel="Fit the whole graph on screen"
|
|
2073
|
+
className={slots.control()}
|
|
2074
|
+
onPress={fitView}
|
|
2075
|
+
>
|
|
2076
|
+
<MaximizeIcon size={16} />
|
|
2077
|
+
</AnimatedPressable>
|
|
2078
|
+
</>
|
|
2079
|
+
) : null}
|
|
2080
|
+
{lock ? (
|
|
2081
|
+
<>
|
|
2082
|
+
<View className="h-px bg-border" />
|
|
2083
|
+
<AnimatedPressable
|
|
2084
|
+
accessibilityRole="button"
|
|
2085
|
+
accessibilityLabel={locked ? 'Unlock the canvas' : 'Lock the canvas'}
|
|
2086
|
+
accessibilityState={{ selected: locked }}
|
|
2087
|
+
className={slots.control()}
|
|
2088
|
+
onPress={() => setLocked(!locked)}
|
|
2089
|
+
>
|
|
2090
|
+
{locked ? <LockIcon size={16} /> : <UnlockIcon size={16} />}
|
|
2091
|
+
</AnimatedPressable>
|
|
2092
|
+
</>
|
|
2093
|
+
) : null}
|
|
2094
|
+
</View>
|
|
2095
|
+
</IconColorProvider>
|
|
2082
2096
|
);
|
|
2083
2097
|
}
|
|
2084
2098
|
FlowControls.displayName = 'Flow.Controls';
|