panelui-native 0.35.2 → 0.38.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 +12 -1
- package/lib/module/components/combobox/index.js +718 -0
- package/lib/module/components/combobox/index.js.map +1 -0
- package/lib/module/components/form/use-field.js +18 -0
- package/lib/module/components/form/use-field.js.map +1 -1
- package/lib/module/components/form/use-form.js +91 -8
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/input/index.js +142 -32
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/otp-input/index.js +4 -1
- package/lib/module/components/otp-input/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +1 -1
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/select/index.js +116 -15
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/textarea/index.js +3 -1
- package/lib/module/components/textarea/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +801 -0
- package/lib/module/components/time-picker/index.js.map +1 -0
- package/lib/module/icons/index.js +59 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/theme/use-theme.js +1 -1
- package/lib/module/utils/time.js +180 -0
- package/lib/module/utils/time.js.map +1 -0
- package/lib/typescript/src/components/combobox/index.d.ts +164 -0
- package/lib/typescript/src/components/combobox/index.d.ts.map +1 -0
- package/lib/typescript/src/components/form/use-field.d.ts.map +1 -1
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts +106 -0
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/select/index.d.ts +32 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/textarea/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts +104 -0
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +4 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/theme/use-theme.d.ts +1 -1
- package/lib/typescript/src/utils/time.d.ts +88 -0
- package/lib/typescript/src/utils/time.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/combobox/index.tsx +927 -0
- package/src/components/form/use-field.ts +23 -0
- package/src/components/form/use-form.ts +106 -16
- package/src/components/input/index.tsx +185 -29
- package/src/components/otp-input/index.tsx +4 -1
- package/src/components/panelside/index.tsx +1 -1
- package/src/components/select/index.tsx +114 -15
- package/src/components/textarea/index.tsx +3 -1
- package/src/components/time-picker/index.tsx +971 -0
- package/src/icons/index.tsx +47 -0
- package/src/index.ts +35 -0
- package/src/theme/use-theme.ts +1 -1
- package/src/utils/time.ts +192 -0
- package/theme.css +32 -0
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TimePicker — a time of day behind a trigger, in one of three faces.
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* const [time, setTime] = useState<TimeValue>();
|
|
8
|
+
*
|
|
9
|
+
* <TimePicker value={time} onValueChange={setTime} />
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The value is `{ hour, minute }` on a 24-hour clock, whatever the face shows.
|
|
13
|
+
* A 12-hour display is a rendering choice; storing 7pm as `{ hour: 7 }` plus a
|
|
14
|
+
* meridiem flag would put the flag into every comparison downstream.
|
|
15
|
+
*
|
|
16
|
+
* ## Three layouts, because "pick a time" is three different tasks
|
|
17
|
+
*
|
|
18
|
+
* - **`wheel`** — hour, minute and meridiem as snapping columns. The precise
|
|
19
|
+
* one: any minute in the day is two or three flicks away. The default.
|
|
20
|
+
* - **`clock`** — a face beside a list of times at a fixed step. For picking a
|
|
21
|
+
* *slot* rather than a time — a booking, a reminder, an appointment — where
|
|
22
|
+
* the face answers "is that morning or evening?" faster than reading digits.
|
|
23
|
+
* - **`ruler`** — one large readout over a swipeable scale. The coarse one, and
|
|
24
|
+
* the only one that reads at arm's length, so it is the one for a sheet with
|
|
25
|
+
* a thumb on it.
|
|
26
|
+
*
|
|
27
|
+
* All three produce the same value and take the same props. Swapping between
|
|
28
|
+
* them is a one-word change, which is the point of them being one component.
|
|
29
|
+
*
|
|
30
|
+
* ## Presentation is separate from layout
|
|
31
|
+
*
|
|
32
|
+
* `popover`, `dialog` and `bottom-sheet` wrap the same panel; `inline` renders
|
|
33
|
+
* it bare, for composing into a Frame or a form. Popover and sheet hand off to
|
|
34
|
+
* `Popover`, which already owns both — but `Popover` has no dialog form, so
|
|
35
|
+
* the switch lives here rather than being pushed down into it. A picker is
|
|
36
|
+
* also the wrong place to widen a general-purpose overlay: the three shapes
|
|
37
|
+
* differ in how they are *dismissed*, not in what they contain.
|
|
38
|
+
*
|
|
39
|
+
* ## Scroll offset, not gesture maths
|
|
40
|
+
*
|
|
41
|
+
* The wheel, the clock's list and the ruler are all scroll views with
|
|
42
|
+
* `snapToOffsets`, and the selection is `Math.round(offset / itemSize)`. That
|
|
43
|
+
* buys momentum, deceleration, edge bounce and platform-correct fling physics
|
|
44
|
+
* for nothing, and none of it would be worth rebuilding on a pan gesture. The
|
|
45
|
+
* per-item fade and the ruler's readout follow the offset on the UI thread, so
|
|
46
|
+
* scrolling never round-trips through React — only the settled value does.
|
|
47
|
+
*/
|
|
48
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
49
|
+
import { ScrollView, View } from 'react-native';
|
|
50
|
+
import Animated, { interpolate, useAnimatedProps, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
51
|
+
import Svg, { Circle, Line } from 'react-native-svg';
|
|
52
|
+
import { useCSSVariable } from 'uniwind';
|
|
53
|
+
import { tv } from 'tailwind-variants';
|
|
54
|
+
import { ClockIcon } from "../../icons/index.js";
|
|
55
|
+
import { Text } from "../../primitives/text.js";
|
|
56
|
+
import { cn } from "../../utils/cn.js";
|
|
57
|
+
import { selectionTick } from "../../utils/haptics.js";
|
|
58
|
+
import { clampTime, displayHour, formatTime, hourFromDisplay, isSameTime, meridiemLabels, meridiemOf, padTwo, roundToStep, timeToMinutes, timesOfDay } from "../../utils/time.js";
|
|
59
|
+
import { Button } from "../button/index.js";
|
|
60
|
+
import { Dialog } from "../dialog/index.js";
|
|
61
|
+
import { Popover } from "../popover/index.js";
|
|
62
|
+
|
|
63
|
+
/** Which face the panel draws. */
|
|
64
|
+
|
|
65
|
+
/** How the panel gets onto the screen. */
|
|
66
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
67
|
+
/** What a closed picker reads when nothing has been chosen. */
|
|
68
|
+
const DEFAULT_PLACEHOLDER = 'Choose a time';
|
|
69
|
+
|
|
70
|
+
/** Row height in every scrolling column. Big enough to hit, small enough to see five. */
|
|
71
|
+
const ROW_HEIGHT = 44;
|
|
72
|
+
/** Rows visible in a column. Odd, so one of them is the centre. */
|
|
73
|
+
const VISIBLE_ROWS = 5;
|
|
74
|
+
const COLUMN_HEIGHT = ROW_HEIGHT * VISIBLE_ROWS;
|
|
75
|
+
|
|
76
|
+
/** Distance between two ruler ticks. */
|
|
77
|
+
const TICK_SPACING = 12;
|
|
78
|
+
|
|
79
|
+
/** Settles the clock hands after a pick. Slow enough to be followed by eye. */
|
|
80
|
+
const HAND_SPRING = {
|
|
81
|
+
damping: 16,
|
|
82
|
+
stiffness: 140,
|
|
83
|
+
mass: 0.7
|
|
84
|
+
};
|
|
85
|
+
const timePickerVariants = tv({
|
|
86
|
+
slots: {
|
|
87
|
+
panel: 'gap-3',
|
|
88
|
+
/* The columns and the highlight are stacked, so the pill is drawn once by
|
|
89
|
+
the container rather than once per column — three separately positioned
|
|
90
|
+
pills cannot be kept flush with each other across a rounding boundary. */
|
|
91
|
+
wheel: 'relative flex-row items-stretch justify-center',
|
|
92
|
+
highlight: 'absolute inset-x-0 rounded-xl bg-muted',
|
|
93
|
+
column: 'flex-1',
|
|
94
|
+
row: 'items-center justify-center',
|
|
95
|
+
rowLabel: 'text-lg tabular-nums text-foreground',
|
|
96
|
+
rowLabelSelected: 'font-semibold text-primary',
|
|
97
|
+
readout: 'text-center text-4xl font-semibold tabular-nums text-foreground',
|
|
98
|
+
clock: 'flex-row items-center gap-4',
|
|
99
|
+
footer: 'flex-row justify-end gap-2'
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/* -------------------------------------------------------------------------- */
|
|
104
|
+
/* One snapping column */
|
|
105
|
+
/* -------------------------------------------------------------------------- */
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A vertical list that comes to rest on a whole row.
|
|
109
|
+
*
|
|
110
|
+
* `snapToOffsets` rather than `snapToInterval`: the two behave the same on a
|
|
111
|
+
* short flick, but an interval lets a hard fling coast past several rows and
|
|
112
|
+
* land between two of them on Android. Explicit offsets plus
|
|
113
|
+
* `disableIntervalMomentum` pin every rest position to a row.
|
|
114
|
+
*
|
|
115
|
+
* Half a column of padding at each end is what lets the first and last items
|
|
116
|
+
* reach the centre — without it, midnight can be scrolled to but never
|
|
117
|
+
* selected, since the list runs out before it gets there.
|
|
118
|
+
*/
|
|
119
|
+
function Column({
|
|
120
|
+
items,
|
|
121
|
+
index,
|
|
122
|
+
onIndexChange,
|
|
123
|
+
label,
|
|
124
|
+
disabled,
|
|
125
|
+
accessibilityLabel,
|
|
126
|
+
className
|
|
127
|
+
}) {
|
|
128
|
+
const ref = useRef(null);
|
|
129
|
+
const offset = useSharedValue(index * ROW_HEIGHT);
|
|
130
|
+
/*
|
|
131
|
+
* The index the list is resting on, tracked separately from the `index`
|
|
132
|
+
* prop. Scrolling writes to it, and the effect below only re-scrolls when
|
|
133
|
+
* the prop disagrees — otherwise every settle would push the list back to
|
|
134
|
+
* where it already is, cancelling the user's own momentum.
|
|
135
|
+
*/
|
|
136
|
+
const resting = useRef(index);
|
|
137
|
+
const snapToOffsets = useMemo(() => items.map((_, i) => i * ROW_HEIGHT), [items]);
|
|
138
|
+
const handler = useAnimatedScrollHandler(event => {
|
|
139
|
+
offset.value = event.contentOffset.y;
|
|
140
|
+
});
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (index === resting.current) return;
|
|
143
|
+
resting.current = index;
|
|
144
|
+
ref.current?.scrollTo({
|
|
145
|
+
y: index * ROW_HEIGHT,
|
|
146
|
+
animated: true
|
|
147
|
+
});
|
|
148
|
+
}, [index]);
|
|
149
|
+
const settle = useCallback(event => {
|
|
150
|
+
const next = Math.round(event.nativeEvent.contentOffset.y / ROW_HEIGHT);
|
|
151
|
+
const clamped = Math.min(Math.max(next, 0), items.length - 1);
|
|
152
|
+
if (clamped === resting.current) return;
|
|
153
|
+
resting.current = clamped;
|
|
154
|
+
selectionTick();
|
|
155
|
+
onIndexChange(clamped);
|
|
156
|
+
}, [items.length, onIndexChange]);
|
|
157
|
+
const selectedItem = items[index];
|
|
158
|
+
return /*#__PURE__*/_jsx(Animated.ScrollView, {
|
|
159
|
+
ref: ref,
|
|
160
|
+
className: className,
|
|
161
|
+
style: {
|
|
162
|
+
height: COLUMN_HEIGHT
|
|
163
|
+
},
|
|
164
|
+
contentContainerStyle: {
|
|
165
|
+
paddingVertical: (COLUMN_HEIGHT - ROW_HEIGHT) / 2
|
|
166
|
+
},
|
|
167
|
+
contentOffset: {
|
|
168
|
+
x: 0,
|
|
169
|
+
y: index * ROW_HEIGHT
|
|
170
|
+
},
|
|
171
|
+
onScroll: handler,
|
|
172
|
+
scrollEventThrottle: 16,
|
|
173
|
+
scrollEnabled: !disabled,
|
|
174
|
+
showsVerticalScrollIndicator: false,
|
|
175
|
+
snapToOffsets: snapToOffsets,
|
|
176
|
+
disableIntervalMomentum: true,
|
|
177
|
+
decelerationRate: "fast"
|
|
178
|
+
/* Both, because only one of them fires: a flick ends in momentum, and a
|
|
179
|
+
slow drag released on a snap point ends without any. Missing the drag
|
|
180
|
+
case leaves a column that looks settled and has reported nothing. */,
|
|
181
|
+
onMomentumScrollEnd: settle,
|
|
182
|
+
onScrollEndDrag: settle
|
|
183
|
+
/* The rows fade rather than unmount, so a recycled row would pop in at
|
|
184
|
+
full opacity mid-scroll. A day of minutes is 60 views; keeping them
|
|
185
|
+
all is cheaper than the flicker. */,
|
|
186
|
+
removeClippedSubviews: false,
|
|
187
|
+
accessibilityRole: "adjustable",
|
|
188
|
+
accessibilityLabel: accessibilityLabel,
|
|
189
|
+
accessibilityValue: selectedItem ? {
|
|
190
|
+
text: label(selectedItem)
|
|
191
|
+
} : undefined,
|
|
192
|
+
children: items.map((item, i) => /*#__PURE__*/_jsx(ColumnRow, {
|
|
193
|
+
offset: offset,
|
|
194
|
+
index: i,
|
|
195
|
+
selected: i === index,
|
|
196
|
+
children: label(item)
|
|
197
|
+
}, i))
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* One row, faded by how far it is from the centre.
|
|
203
|
+
*
|
|
204
|
+
* The fade is a function of the live scroll offset rather than of the selected
|
|
205
|
+
* index, so it tracks the finger instead of stepping once per settle — which
|
|
206
|
+
* is the difference between a wheel and a list that changes colour.
|
|
207
|
+
*/
|
|
208
|
+
function ColumnRow({
|
|
209
|
+
offset,
|
|
210
|
+
index,
|
|
211
|
+
selected,
|
|
212
|
+
children
|
|
213
|
+
}) {
|
|
214
|
+
const slots = timePickerVariants();
|
|
215
|
+
const style = useAnimatedStyle(() => {
|
|
216
|
+
const distance = Math.abs(offset.value / ROW_HEIGHT - index);
|
|
217
|
+
return {
|
|
218
|
+
opacity: interpolate(distance, [0, 1, 2], [1, 0.55, 0.2], 'clamp'),
|
|
219
|
+
transform: [{
|
|
220
|
+
scale: interpolate(distance, [0, 1], [1, 0.88], 'clamp')
|
|
221
|
+
}]
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
225
|
+
style: [style, {
|
|
226
|
+
height: ROW_HEIGHT
|
|
227
|
+
}],
|
|
228
|
+
className: slots.row(),
|
|
229
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
230
|
+
className: cn(slots.rowLabel(), selected && slots.rowLabelSelected()),
|
|
231
|
+
children: children
|
|
232
|
+
})
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* -------------------------------------------------------------------------- */
|
|
237
|
+
/* wheel */
|
|
238
|
+
/* -------------------------------------------------------------------------- */
|
|
239
|
+
|
|
240
|
+
function WheelFace({
|
|
241
|
+
value,
|
|
242
|
+
onValueChange,
|
|
243
|
+
hourCycle,
|
|
244
|
+
minuteStep,
|
|
245
|
+
locale,
|
|
246
|
+
disabled
|
|
247
|
+
}) {
|
|
248
|
+
const slots = timePickerVariants();
|
|
249
|
+
const [am, pm] = useMemo(() => meridiemLabels(locale), [locale]);
|
|
250
|
+
const hours = useMemo(() => hourCycle === 24 ? Array.from({
|
|
251
|
+
length: 24
|
|
252
|
+
}, (_, i) => i) : Array.from({
|
|
253
|
+
length: 12
|
|
254
|
+
}, (_, i) => i + 1), [hourCycle]);
|
|
255
|
+
const minutes = useMemo(() => Array.from({
|
|
256
|
+
length: Math.ceil(60 / minuteStep)
|
|
257
|
+
}, (_, i) => i * minuteStep), [minuteStep]);
|
|
258
|
+
const meridiems = useMemo(() => [am, pm], [am, pm]);
|
|
259
|
+
const shownHour = displayHour(value.hour, hourCycle);
|
|
260
|
+
const hourIndex = Math.max(0, hours.indexOf(shownHour));
|
|
261
|
+
/*
|
|
262
|
+
* Nearest rather than exact. A minute that is not on the step — from a
|
|
263
|
+
* default value, or from `minuteStep` changing under a chosen time — has no
|
|
264
|
+
* row of its own, and `indexOf` returning -1 would park the column at
|
|
265
|
+
* midnight rather than at the closest minute it can actually offer.
|
|
266
|
+
*/
|
|
267
|
+
const minuteIndex = Math.min(minutes.length - 1, Math.round(value.minute / minuteStep));
|
|
268
|
+
const meridiemIndex = meridiemOf(value.hour) === 'am' ? 0 : 1;
|
|
269
|
+
const setHour = useCallback(index => {
|
|
270
|
+
const displayed = hours[index] ?? 0;
|
|
271
|
+
onValueChange({
|
|
272
|
+
...value,
|
|
273
|
+
hour: hourFromDisplay(displayed, meridiemOf(value.hour), hourCycle)
|
|
274
|
+
});
|
|
275
|
+
}, [hours, hourCycle, onValueChange, value]);
|
|
276
|
+
const setMinute = useCallback(index => onValueChange({
|
|
277
|
+
...value,
|
|
278
|
+
minute: minutes[index] ?? 0
|
|
279
|
+
}), [minutes, onValueChange, value]);
|
|
280
|
+
const setMeridiem = useCallback(index => {
|
|
281
|
+
const next = index === 0 ? 'am' : 'pm';
|
|
282
|
+
if (next === meridiemOf(value.hour)) return;
|
|
283
|
+
onValueChange({
|
|
284
|
+
...value,
|
|
285
|
+
hour: next === 'pm' ? value.hour + 12 : value.hour - 12
|
|
286
|
+
});
|
|
287
|
+
}, [onValueChange, value]);
|
|
288
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
289
|
+
className: slots.wheel(),
|
|
290
|
+
style: {
|
|
291
|
+
height: COLUMN_HEIGHT
|
|
292
|
+
},
|
|
293
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
294
|
+
pointerEvents: "none",
|
|
295
|
+
className: slots.highlight(),
|
|
296
|
+
style: {
|
|
297
|
+
top: (COLUMN_HEIGHT - ROW_HEIGHT) / 2,
|
|
298
|
+
height: ROW_HEIGHT
|
|
299
|
+
}
|
|
300
|
+
}), /*#__PURE__*/_jsx(Column, {
|
|
301
|
+
items: hours,
|
|
302
|
+
index: hourIndex,
|
|
303
|
+
onIndexChange: setHour,
|
|
304
|
+
label: hour => hourCycle === 24 ? padTwo(hour) : String(hour),
|
|
305
|
+
disabled: disabled,
|
|
306
|
+
accessibilityLabel: "Hour",
|
|
307
|
+
className: slots.column()
|
|
308
|
+
}), /*#__PURE__*/_jsx(Column, {
|
|
309
|
+
items: minutes,
|
|
310
|
+
index: minuteIndex,
|
|
311
|
+
onIndexChange: setMinute,
|
|
312
|
+
label: padTwo,
|
|
313
|
+
disabled: disabled,
|
|
314
|
+
accessibilityLabel: "Minute",
|
|
315
|
+
className: slots.column()
|
|
316
|
+
}), hourCycle === 12 ? /*#__PURE__*/_jsx(Column, {
|
|
317
|
+
items: meridiems,
|
|
318
|
+
index: meridiemIndex,
|
|
319
|
+
onIndexChange: setMeridiem,
|
|
320
|
+
label: entry => entry,
|
|
321
|
+
disabled: disabled,
|
|
322
|
+
accessibilityLabel: "AM or PM",
|
|
323
|
+
className: slots.column()
|
|
324
|
+
}) : null]
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* -------------------------------------------------------------------------- */
|
|
329
|
+
/* clock */
|
|
330
|
+
/* -------------------------------------------------------------------------- */
|
|
331
|
+
|
|
332
|
+
const FACE_SIZE = 132;
|
|
333
|
+
const FACE_CENTRE = FACE_SIZE / 2;
|
|
334
|
+
const AnimatedLine = Animated.createAnimatedComponent(Line);
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* An analog face whose hands follow the selection.
|
|
338
|
+
*
|
|
339
|
+
* The hands are animated rather than snapped because the face's whole job is
|
|
340
|
+
* to say *when in the day* the highlighted row is, and a hand that jumps gives
|
|
341
|
+
* that away one row at a time. Sweeping, you read the shape of the movement
|
|
342
|
+
* and stop looking at the digits.
|
|
343
|
+
*/
|
|
344
|
+
function ClockFace({
|
|
345
|
+
value
|
|
346
|
+
}) {
|
|
347
|
+
// `useCSSVariable` is typed for every kind of token, so a colour has to be
|
|
348
|
+
// narrowed before SVG will take it.
|
|
349
|
+
const rawTick = useCSSVariable('--color-muted-foreground');
|
|
350
|
+
const rawHand = useCSSVariable('--color-foreground');
|
|
351
|
+
const rawDial = useCSSVariable('--color-muted');
|
|
352
|
+
const tick = typeof rawTick === 'string' ? rawTick : undefined;
|
|
353
|
+
const hand = typeof rawHand === 'string' ? rawHand : undefined;
|
|
354
|
+
const dial = typeof rawDial === 'string' ? rawDial : undefined;
|
|
355
|
+
|
|
356
|
+
/*
|
|
357
|
+
* Total minutes, not the hour and minute separately. At five to twelve the
|
|
358
|
+
* hour hand is nearly at twelve, and driving it from `hour` alone leaves it
|
|
359
|
+
* pointing at eleven until the minute hand completes the turn.
|
|
360
|
+
*/
|
|
361
|
+
const minutes = timeToMinutes(value);
|
|
362
|
+
const progress = useSharedValue(minutes);
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
progress.value = withSpring(minutes, HAND_SPRING);
|
|
365
|
+
}, [minutes, progress]);
|
|
366
|
+
|
|
367
|
+
/*
|
|
368
|
+
* `animatedProps` rather than a transform: an SVG line has no transform
|
|
369
|
+
* origin of its own, so rotating it turns it about the origin of the whole
|
|
370
|
+
* canvas rather than about the pin at the centre of the dial. Moving the end
|
|
371
|
+
* point is the same maths without the frame-of-reference problem.
|
|
372
|
+
*
|
|
373
|
+
* The hands run on the *whole* turn, so the wrap at twelve and at the hour
|
|
374
|
+
* sweeps back rather than jumping — which is what a real hand does.
|
|
375
|
+
*/
|
|
376
|
+
const hourHand = useAnimatedProps(() => handEnd(progress.value % 720 / 720, 34));
|
|
377
|
+
const minuteHand = useAnimatedProps(() => handEnd(progress.value % 60 / 60, 48));
|
|
378
|
+
return /*#__PURE__*/_jsxs(Svg, {
|
|
379
|
+
width: FACE_SIZE,
|
|
380
|
+
height: FACE_SIZE,
|
|
381
|
+
children: [/*#__PURE__*/_jsx(Circle, {
|
|
382
|
+
cx: FACE_CENTRE,
|
|
383
|
+
cy: FACE_CENTRE,
|
|
384
|
+
r: FACE_CENTRE - 2,
|
|
385
|
+
fill: dial
|
|
386
|
+
}), Array.from({
|
|
387
|
+
length: 12
|
|
388
|
+
}, (_, i) => {
|
|
389
|
+
const angle = i / 12 * 2 * Math.PI - Math.PI / 2;
|
|
390
|
+
const outer = FACE_CENTRE - 10;
|
|
391
|
+
const inner = outer - (i % 3 === 0 ? 9 : 6);
|
|
392
|
+
return /*#__PURE__*/_jsx(Line, {
|
|
393
|
+
x1: FACE_CENTRE + Math.cos(angle) * inner,
|
|
394
|
+
y1: FACE_CENTRE + Math.sin(angle) * inner,
|
|
395
|
+
x2: FACE_CENTRE + Math.cos(angle) * outer,
|
|
396
|
+
y2: FACE_CENTRE + Math.sin(angle) * outer,
|
|
397
|
+
stroke: tick,
|
|
398
|
+
strokeWidth: i % 3 === 0 ? 2 : 1,
|
|
399
|
+
strokeLinecap: "round"
|
|
400
|
+
}, i);
|
|
401
|
+
}), /*#__PURE__*/_jsx(AnimatedLine, {
|
|
402
|
+
animatedProps: hourHand,
|
|
403
|
+
stroke: hand,
|
|
404
|
+
strokeWidth: 3,
|
|
405
|
+
strokeLinecap: "round"
|
|
406
|
+
}), /*#__PURE__*/_jsx(AnimatedLine, {
|
|
407
|
+
animatedProps: minuteHand,
|
|
408
|
+
stroke: hand,
|
|
409
|
+
strokeWidth: 2,
|
|
410
|
+
strokeLinecap: "round"
|
|
411
|
+
}), /*#__PURE__*/_jsx(Circle, {
|
|
412
|
+
cx: FACE_CENTRE,
|
|
413
|
+
cy: FACE_CENTRE,
|
|
414
|
+
r: 3,
|
|
415
|
+
fill: hand
|
|
416
|
+
})]
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Where a hand of `length` ends, given how far round the dial it has turned.
|
|
422
|
+
*
|
|
423
|
+
* A worklet, so both hands can be computed on the UI thread. Twelve o'clock is
|
|
424
|
+
* straight up and SVG's zero angle is to the right, hence the quarter turn.
|
|
425
|
+
*/
|
|
426
|
+
function handEnd(turn, length) {
|
|
427
|
+
'worklet';
|
|
428
|
+
|
|
429
|
+
const angle = turn * 2 * Math.PI - Math.PI / 2;
|
|
430
|
+
return {
|
|
431
|
+
x1: FACE_CENTRE,
|
|
432
|
+
y1: FACE_CENTRE,
|
|
433
|
+
x2: FACE_CENTRE + Math.cos(angle) * length,
|
|
434
|
+
y2: FACE_CENTRE + Math.sin(angle) * length
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function ClockFaceLayout({
|
|
438
|
+
value,
|
|
439
|
+
onValueChange,
|
|
440
|
+
hourCycle,
|
|
441
|
+
minuteStep,
|
|
442
|
+
locale,
|
|
443
|
+
disabled
|
|
444
|
+
}) {
|
|
445
|
+
const slots = timePickerVariants();
|
|
446
|
+
const times = useMemo(() => timesOfDay(minuteStep), [minuteStep]);
|
|
447
|
+
const index = Math.min(times.length - 1, Math.round(timeToMinutes(value) / minuteStep));
|
|
448
|
+
const setIndex = useCallback(next => {
|
|
449
|
+
const time = times[next];
|
|
450
|
+
if (time) onValueChange(time);
|
|
451
|
+
}, [onValueChange, times]);
|
|
452
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
453
|
+
className: slots.clock(),
|
|
454
|
+
children: [/*#__PURE__*/_jsx(ClockFace, {
|
|
455
|
+
value: value
|
|
456
|
+
}), /*#__PURE__*/_jsx(Column, {
|
|
457
|
+
items: times,
|
|
458
|
+
index: index,
|
|
459
|
+
onIndexChange: setIndex,
|
|
460
|
+
label: time => formatTime(time, {
|
|
461
|
+
hourCycle,
|
|
462
|
+
locale
|
|
463
|
+
}),
|
|
464
|
+
disabled: disabled,
|
|
465
|
+
accessibilityLabel: "Time",
|
|
466
|
+
className: slots.column()
|
|
467
|
+
})]
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* -------------------------------------------------------------------------- */
|
|
472
|
+
/* ruler */
|
|
473
|
+
/* -------------------------------------------------------------------------- */
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* One big readout over a scale you swipe.
|
|
477
|
+
*
|
|
478
|
+
* The scale is a horizontal scroll view with a tick per step and half its
|
|
479
|
+
* width of padding at each end, so the first and last times can reach the
|
|
480
|
+
* centre line — the same trick the columns use, on the other axis.
|
|
481
|
+
*
|
|
482
|
+
* The readout is a plain `Text` driven by React rather than an animated one
|
|
483
|
+
* driven by the offset. It only has to be right when the scale comes to rest,
|
|
484
|
+
* and re-rendering one string per settled step is cheaper than keeping a
|
|
485
|
+
* formatted time on the UI thread, where `Intl` cannot go.
|
|
486
|
+
*/
|
|
487
|
+
function RulerFace({
|
|
488
|
+
value,
|
|
489
|
+
onValueChange,
|
|
490
|
+
hourCycle,
|
|
491
|
+
minuteStep,
|
|
492
|
+
locale,
|
|
493
|
+
disabled
|
|
494
|
+
}) {
|
|
495
|
+
const slots = timePickerVariants();
|
|
496
|
+
const ref = useRef(null);
|
|
497
|
+
const times = useMemo(() => timesOfDay(minuteStep), [minuteStep]);
|
|
498
|
+
const index = Math.min(times.length - 1, Math.round(timeToMinutes(value) / minuteStep));
|
|
499
|
+
const resting = useRef(index);
|
|
500
|
+
/*
|
|
501
|
+
* Measured, not assumed: the scroller is narrower than the panel by whatever
|
|
502
|
+
* padding the presentation put around it, which this component cannot see.
|
|
503
|
+
*
|
|
504
|
+
* Half the scroller, *less half a tick*. A tick is drawn in the middle of a
|
|
505
|
+
* cell `TICK_SPACING` wide, so padding by half the width alone puts the
|
|
506
|
+
* cell's leading edge under the indicator and the tick itself half a cell to
|
|
507
|
+
* the right of it — close enough to look like a rounding error and wrong at
|
|
508
|
+
* every rest position.
|
|
509
|
+
*/
|
|
510
|
+
const [width, setWidth] = useState(0);
|
|
511
|
+
const pad = Math.max(0, width / 2 - TICK_SPACING / 2);
|
|
512
|
+
const snapToOffsets = useMemo(() => times.map((_, i) => i * TICK_SPACING), [times]);
|
|
513
|
+
useEffect(() => {
|
|
514
|
+
if (index === resting.current) return;
|
|
515
|
+
resting.current = index;
|
|
516
|
+
ref.current?.scrollTo({
|
|
517
|
+
x: index * TICK_SPACING,
|
|
518
|
+
animated: true
|
|
519
|
+
});
|
|
520
|
+
}, [index]);
|
|
521
|
+
const settle = useCallback(event => {
|
|
522
|
+
const next = Math.round(event.nativeEvent.contentOffset.x / TICK_SPACING);
|
|
523
|
+
const clamped = Math.min(Math.max(next, 0), times.length - 1);
|
|
524
|
+
if (clamped === resting.current) return;
|
|
525
|
+
resting.current = clamped;
|
|
526
|
+
selectionTick();
|
|
527
|
+
const time = times[clamped];
|
|
528
|
+
if (time) onValueChange(time);
|
|
529
|
+
}, [onValueChange, times]);
|
|
530
|
+
const stepsPerHour = Math.max(1, Math.round(60 / minuteStep));
|
|
531
|
+
|
|
532
|
+
/*
|
|
533
|
+
* `contentOffset` only takes effect on mount, and on mount the padding is
|
|
534
|
+
* still zero because the width has not been measured — so the resting
|
|
535
|
+
* position has to be set again once it has. Unanimated: this is the first
|
|
536
|
+
* frame the scale is correctly laid out in, not a movement.
|
|
537
|
+
*/
|
|
538
|
+
const onLayout = useCallback(event => {
|
|
539
|
+
const next = event.nativeEvent.layout.width;
|
|
540
|
+
if (next === width) return;
|
|
541
|
+
setWidth(next);
|
|
542
|
+
ref.current?.scrollTo({
|
|
543
|
+
x: resting.current * TICK_SPACING,
|
|
544
|
+
animated: false
|
|
545
|
+
});
|
|
546
|
+
}, [width]);
|
|
547
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
548
|
+
className: "gap-4",
|
|
549
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
550
|
+
className: slots.readout(),
|
|
551
|
+
children: formatTime(value, {
|
|
552
|
+
hourCycle,
|
|
553
|
+
locale
|
|
554
|
+
})
|
|
555
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
556
|
+
className: "relative h-14 justify-center",
|
|
557
|
+
onLayout: onLayout,
|
|
558
|
+
children: [/*#__PURE__*/_jsx(ScrollView, {
|
|
559
|
+
ref: ref,
|
|
560
|
+
horizontal: true,
|
|
561
|
+
contentContainerStyle: {
|
|
562
|
+
paddingHorizontal: pad
|
|
563
|
+
},
|
|
564
|
+
contentOffset: {
|
|
565
|
+
x: index * TICK_SPACING,
|
|
566
|
+
y: 0
|
|
567
|
+
},
|
|
568
|
+
scrollEnabled: !disabled,
|
|
569
|
+
showsHorizontalScrollIndicator: false,
|
|
570
|
+
snapToOffsets: snapToOffsets,
|
|
571
|
+
disableIntervalMomentum: true,
|
|
572
|
+
decelerationRate: "fast",
|
|
573
|
+
onMomentumScrollEnd: settle,
|
|
574
|
+
onScrollEndDrag: settle,
|
|
575
|
+
accessibilityRole: "adjustable",
|
|
576
|
+
accessibilityLabel: "Time",
|
|
577
|
+
accessibilityValue: {
|
|
578
|
+
text: formatTime(value, {
|
|
579
|
+
hourCycle,
|
|
580
|
+
locale
|
|
581
|
+
})
|
|
582
|
+
},
|
|
583
|
+
children: times.map((_, i) => /*#__PURE__*/_jsx(View, {
|
|
584
|
+
style: {
|
|
585
|
+
width: TICK_SPACING
|
|
586
|
+
},
|
|
587
|
+
className: "items-center justify-center",
|
|
588
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
589
|
+
className: cn('w-0.5 rounded-full',
|
|
590
|
+
// Taller and darker on the hour, so the scale reads as hours
|
|
591
|
+
// rather than as an undifferentiated comb.
|
|
592
|
+
i % stepsPerHour === 0 ? 'h-7 bg-muted-foreground' : 'h-4 bg-border')
|
|
593
|
+
})
|
|
594
|
+
}, i))
|
|
595
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
596
|
+
pointerEvents: "none",
|
|
597
|
+
className: "absolute inset-y-1 left-1/2 w-0.5 -translate-x-px rounded-full bg-primary"
|
|
598
|
+
})]
|
|
599
|
+
})]
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/* -------------------------------------------------------------------------- */
|
|
604
|
+
/* The panel */
|
|
605
|
+
/* -------------------------------------------------------------------------- */
|
|
606
|
+
|
|
607
|
+
/** The width the panel asks for. Wide enough for three columns of digits. */
|
|
608
|
+
const PANEL_WIDTH = 300;
|
|
609
|
+
function Panel({
|
|
610
|
+
layout,
|
|
611
|
+
className,
|
|
612
|
+
fullWidth,
|
|
613
|
+
footer,
|
|
614
|
+
...face
|
|
615
|
+
}) {
|
|
616
|
+
const slots = timePickerVariants();
|
|
617
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
618
|
+
className: cn(slots.panel(), className),
|
|
619
|
+
style: fullWidth ? undefined : {
|
|
620
|
+
width: PANEL_WIDTH
|
|
621
|
+
},
|
|
622
|
+
children: [layout === 'wheel' ? /*#__PURE__*/_jsx(WheelFace, {
|
|
623
|
+
...face
|
|
624
|
+
}) : null, layout === 'clock' ? /*#__PURE__*/_jsx(ClockFaceLayout, {
|
|
625
|
+
...face
|
|
626
|
+
}) : null, layout === 'ruler' ? /*#__PURE__*/_jsx(RulerFace, {
|
|
627
|
+
...face
|
|
628
|
+
}) : null, footer]
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* -------------------------------------------------------------------------- */
|
|
633
|
+
/* TimePicker */
|
|
634
|
+
/* -------------------------------------------------------------------------- */
|
|
635
|
+
|
|
636
|
+
/** The step each layout picks when the caller does not. */
|
|
637
|
+
const DEFAULT_STEP = {
|
|
638
|
+
wheel: 1,
|
|
639
|
+
clock: 30,
|
|
640
|
+
ruler: 15
|
|
641
|
+
};
|
|
642
|
+
function TimePickerRoot({
|
|
643
|
+
value: valueProp,
|
|
644
|
+
defaultValue,
|
|
645
|
+
onValueChange,
|
|
646
|
+
layout = 'wheel',
|
|
647
|
+
presentation = 'popover',
|
|
648
|
+
open: openProp,
|
|
649
|
+
onOpenChange,
|
|
650
|
+
hourCycle = 12,
|
|
651
|
+
minuteStep,
|
|
652
|
+
minTime,
|
|
653
|
+
maxTime,
|
|
654
|
+
placeholder = DEFAULT_PLACEHOLDER,
|
|
655
|
+
format,
|
|
656
|
+
locale,
|
|
657
|
+
disabled = false,
|
|
658
|
+
className,
|
|
659
|
+
children
|
|
660
|
+
}) {
|
|
661
|
+
const step = minuteStep ?? DEFAULT_STEP[layout];
|
|
662
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
663
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
664
|
+
const isValueControlled = valueProp !== undefined;
|
|
665
|
+
const isOpenControlled = openProp !== undefined;
|
|
666
|
+
const selected = isValueControlled ? valueProp : internalValue;
|
|
667
|
+
const open = isOpenControlled ? openProp : internalOpen;
|
|
668
|
+
|
|
669
|
+
/*
|
|
670
|
+
* What the faces scroll to before anything has been picked. The nearest
|
|
671
|
+
* allowed time to the top of the current hour, so an unset picker opens
|
|
672
|
+
* somewhere plausible rather than at midnight — and inside the span, since
|
|
673
|
+
* scrolling to a time the caller has forbidden is a worse first frame.
|
|
674
|
+
*/
|
|
675
|
+
const fallback = useMemo(() => {
|
|
676
|
+
const now = new Date();
|
|
677
|
+
return clampTime(roundToStep({
|
|
678
|
+
hour: now.getHours(),
|
|
679
|
+
minute: 0
|
|
680
|
+
}, step), minTime, maxTime);
|
|
681
|
+
}, [step, minTime, maxTime]);
|
|
682
|
+
const draft = selected ?? fallback;
|
|
683
|
+
const setOpen = useCallback(next => {
|
|
684
|
+
if (!isOpenControlled) setInternalOpen(next);
|
|
685
|
+
onOpenChange?.(next);
|
|
686
|
+
}, [isOpenControlled, onOpenChange]);
|
|
687
|
+
|
|
688
|
+
/*
|
|
689
|
+
* Clamped and stepped on the way out, not on the way in. A face reports the
|
|
690
|
+
* row it landed on, and it does not know about `minTime` or a step it is not
|
|
691
|
+
* itself using — putting both here means every layout is bounded by the same
|
|
692
|
+
* rule and none of them has to carry it.
|
|
693
|
+
*/
|
|
694
|
+
const commit = useCallback(next => {
|
|
695
|
+
const bounded = clampTime(roundToStep(next, step), minTime, maxTime);
|
|
696
|
+
if (isSameTime(bounded, selected)) return;
|
|
697
|
+
if (!isValueControlled) setInternalValue(bounded);
|
|
698
|
+
onValueChange?.(bounded);
|
|
699
|
+
}, [isValueControlled, maxTime, minTime, onValueChange, selected, step]);
|
|
700
|
+
const label = useMemo(() => {
|
|
701
|
+
if (!selected) return null;
|
|
702
|
+
if (format) return format(selected);
|
|
703
|
+
return formatTime(selected, {
|
|
704
|
+
hourCycle,
|
|
705
|
+
locale
|
|
706
|
+
});
|
|
707
|
+
}, [format, hourCycle, locale, selected]);
|
|
708
|
+
const face = {
|
|
709
|
+
value: draft,
|
|
710
|
+
onValueChange: commit,
|
|
711
|
+
hourCycle,
|
|
712
|
+
minuteStep: step,
|
|
713
|
+
locale,
|
|
714
|
+
disabled
|
|
715
|
+
};
|
|
716
|
+
if (presentation === 'inline') {
|
|
717
|
+
return /*#__PURE__*/_jsx(Panel, {
|
|
718
|
+
layout: layout,
|
|
719
|
+
className: className,
|
|
720
|
+
...face
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
const trigger = children ?? /*#__PURE__*/_jsxs(Button, {
|
|
724
|
+
variant: "outline",
|
|
725
|
+
disabled: disabled,
|
|
726
|
+
className: cn('justify-start gap-2', className),
|
|
727
|
+
children: [/*#__PURE__*/_jsx(ClockIcon, {
|
|
728
|
+
size: 16
|
|
729
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
730
|
+
className: label ? undefined : 'text-muted-foreground',
|
|
731
|
+
children: label ?? placeholder
|
|
732
|
+
})]
|
|
733
|
+
});
|
|
734
|
+
if (presentation === 'dialog') {
|
|
735
|
+
return /*#__PURE__*/_jsxs(Dialog, {
|
|
736
|
+
open: open,
|
|
737
|
+
onOpenChange: setOpen,
|
|
738
|
+
children: [/*#__PURE__*/_jsx(Dialog.Trigger, {
|
|
739
|
+
children: trigger
|
|
740
|
+
}), /*#__PURE__*/_jsx(Dialog.Content, {
|
|
741
|
+
blur: true,
|
|
742
|
+
className: "items-center gap-0 p-4",
|
|
743
|
+
children: /*#__PURE__*/_jsx(Panel, {
|
|
744
|
+
layout: layout,
|
|
745
|
+
...face,
|
|
746
|
+
footer:
|
|
747
|
+
/*#__PURE__*/
|
|
748
|
+
// Full width, like the sheet's. It is the only tap target in the
|
|
749
|
+
// dialog, and a small button in a corner of one asks to be
|
|
750
|
+
// aimed at.
|
|
751
|
+
_jsx(Dialog.Close, {
|
|
752
|
+
children: /*#__PURE__*/_jsx(Button, {
|
|
753
|
+
className: "mt-1 w-full",
|
|
754
|
+
children: "Done"
|
|
755
|
+
})
|
|
756
|
+
})
|
|
757
|
+
})
|
|
758
|
+
})]
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
const isSheet = presentation === 'bottom-sheet';
|
|
762
|
+
|
|
763
|
+
/*
|
|
764
|
+
* The sheet gets a Done button and the popover does not. A popover is
|
|
765
|
+
* dismissed by tapping anywhere outside it, which is most of the screen; a
|
|
766
|
+
* sheet's outside is the strip above it, and a picker whose scale is under
|
|
767
|
+
* your thumb needs somewhere deliberate to finish.
|
|
768
|
+
*
|
|
769
|
+
* Full width and set apart from the face rather than tucked under it. It is
|
|
770
|
+
* the only tap target in the sheet, and at the bottom of the screen it is
|
|
771
|
+
* also the one under the thumb already holding the phone.
|
|
772
|
+
*/
|
|
773
|
+
const footer = isSheet ? /*#__PURE__*/_jsx(Popover.Close, {
|
|
774
|
+
children: /*#__PURE__*/_jsx(Button, {
|
|
775
|
+
className: "mt-2 w-full",
|
|
776
|
+
children: "Done"
|
|
777
|
+
})
|
|
778
|
+
}) : null;
|
|
779
|
+
return /*#__PURE__*/_jsxs(Popover, {
|
|
780
|
+
open: open,
|
|
781
|
+
onOpenChange: setOpen,
|
|
782
|
+
presentation: isSheet ? 'bottom-sheet' : 'popover',
|
|
783
|
+
children: [/*#__PURE__*/_jsx(Popover.Trigger, {
|
|
784
|
+
children: trigger
|
|
785
|
+
}), /*#__PURE__*/_jsx(Popover.Content, {
|
|
786
|
+
width: isSheet ? 'full' : 'content-fit',
|
|
787
|
+
children: /*#__PURE__*/_jsx(Panel, {
|
|
788
|
+
layout: layout,
|
|
789
|
+
fullWidth: isSheet,
|
|
790
|
+
className: isSheet ? 'w-full gap-4 px-1 pb-2' : 'self-center p-3',
|
|
791
|
+
footer: footer,
|
|
792
|
+
...face
|
|
793
|
+
})
|
|
794
|
+
})]
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
TimePickerRoot.displayName = 'TimePicker';
|
|
798
|
+
export const TimePicker = Object.assign(TimePickerRoot, {
|
|
799
|
+
Trigger: Popover.Trigger
|
|
800
|
+
});
|
|
801
|
+
//# sourceMappingURL=index.js.map
|