panelui-native 0.51.1 → 0.54.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.
Files changed (46) hide show
  1. package/README.md +4 -1
  2. package/lib/module/components/candlestick-chart/index.js +1161 -0
  3. package/lib/module/components/candlestick-chart/index.js.map +1 -0
  4. package/lib/module/components/combobox/index.js +73 -7
  5. package/lib/module/components/combobox/index.js.map +1 -1
  6. package/lib/module/components/context-menu/index.js +529 -0
  7. package/lib/module/components/context-menu/index.js.map +1 -0
  8. package/lib/module/components/menu/index.js +18 -10
  9. package/lib/module/components/menu/index.js.map +1 -1
  10. package/lib/module/components/popover/index.js +71 -7
  11. package/lib/module/components/popover/index.js.map +1 -1
  12. package/lib/module/components/sortable/index.js +159 -23
  13. package/lib/module/components/sortable/index.js.map +1 -1
  14. package/lib/module/components/tabs/index.js +56 -15
  15. package/lib/module/components/tabs/index.js.map +1 -1
  16. package/lib/module/components/time-picker/index.js +295 -31
  17. package/lib/module/components/time-picker/index.js.map +1 -1
  18. package/lib/module/index.js +3 -1
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/typescript/src/components/candlestick-chart/index.d.ts +278 -0
  21. package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -0
  22. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
  23. package/lib/typescript/src/components/context-menu/index.d.ts +270 -0
  24. package/lib/typescript/src/components/context-menu/index.d.ts.map +1 -0
  25. package/lib/typescript/src/components/menu/index.d.ts +20 -20
  26. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  27. package/lib/typescript/src/components/popover/index.d.ts +51 -1
  28. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  29. package/lib/typescript/src/components/sortable/index.d.ts +13 -3
  30. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
  31. package/lib/typescript/src/components/tabs/index.d.ts +34 -2
  32. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  33. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  34. package/lib/typescript/src/index.d.ts +3 -1
  35. package/lib/typescript/src/index.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/components/candlestick-chart/index.tsx +1360 -0
  38. package/src/components/combobox/index.tsx +149 -18
  39. package/src/components/context-menu/index.tsx +658 -0
  40. package/src/components/menu/index.tsx +17 -10
  41. package/src/components/popover/index.tsx +94 -6
  42. package/src/components/sortable/index.tsx +278 -36
  43. package/src/components/tabs/index.tsx +82 -16
  44. package/src/components/tag-input/index.tsx +619 -0
  45. package/src/components/time-picker/index.tsx +330 -35
  46. package/src/index.ts +35 -0
@@ -153,6 +153,58 @@ const timePickerVariants = tv({
153
153
  },
154
154
  });
155
155
 
156
+ /**
157
+ * The times a face may offer, and where a value sits among them.
158
+ *
159
+ * A scale or a list that runs the whole day while the picker only accepts part
160
+ * of it is a control that invites a choice in order to refuse it: the finger
161
+ * reaches the end, the root clamps, and the row springs back to where it
162
+ * started having said nothing about why. Offering only what will be accepted
163
+ * means the end of the scale *is* the last bookable time, and dragging to it
164
+ * picks it.
165
+ *
166
+ * The position is the nearest row rather than a division, because a filtered
167
+ * list no longer starts at midnight and its indices are its own.
168
+ */
169
+ function useBoundedTimes(
170
+ minuteStep: number,
171
+ minTime: TimeValue | undefined,
172
+ maxTime: TimeValue | undefined,
173
+ value: TimeValue
174
+ ) {
175
+ const times = useMemo(() => {
176
+ const all = timesOfDay(minuteStep);
177
+ if (!minTime && !maxTime) return all;
178
+ const low = minTime ? timeToMinutes(minTime) : Number.NEGATIVE_INFINITY;
179
+ const high = maxTime ? timeToMinutes(maxTime) : Number.POSITIVE_INFINITY;
180
+ const kept = all.filter((time) => {
181
+ const at = timeToMinutes(time);
182
+ return at >= low && at <= high;
183
+ });
184
+ // A span too narrow to contain a single step would leave nothing to scroll.
185
+ // The bounds are the caller's mistake there, and an empty face is worse.
186
+ return kept.length > 0 ? kept : all;
187
+ }, [minuteStep, minTime, maxTime]);
188
+
189
+ const index = useMemo(() => {
190
+ const target = timeToMinutes(value);
191
+ let nearest = 0;
192
+ let smallest = Number.POSITIVE_INFINITY;
193
+ for (let i = 0; i < times.length; i += 1) {
194
+ const time = times[i];
195
+ if (!time) continue;
196
+ const away = Math.abs(timeToMinutes(time) - target);
197
+ if (away < smallest) {
198
+ smallest = away;
199
+ nearest = i;
200
+ }
201
+ }
202
+ return nearest;
203
+ }, [times, value]);
204
+
205
+ return { times, index };
206
+ }
207
+
156
208
  /* -------------------------------------------------------------------------- */
157
209
  /* One snapping column */
158
210
  /* -------------------------------------------------------------------------- */
@@ -161,6 +213,12 @@ interface ColumnProps<T> {
161
213
  items: readonly T[];
162
214
  /** Index of the selected item. Drives the scroll position. */
163
215
  index: number;
216
+ /**
217
+ * Bumped every time the root has looked at a reported time, whether or not
218
+ * it accepted one. See `Column`'s resync effect for why the index alone is
219
+ * not enough to know a column is resting somewhere it should not be.
220
+ */
221
+ syncToken: number;
164
222
  onIndexChange: (index: number) => void;
165
223
  label: (item: T) => string;
166
224
  disabled?: boolean;
@@ -183,6 +241,7 @@ interface ColumnProps<T> {
183
241
  function Column<T>({
184
242
  items,
185
243
  index,
244
+ syncToken,
186
245
  onIndexChange,
187
246
  label,
188
247
  disabled,
@@ -208,24 +267,135 @@ function Column<T>({
208
267
  offset.value = event.contentOffset.y;
209
268
  });
210
269
 
270
+ /*
271
+ * Whether the list is still moving, and whether it owes itself a correction
272
+ * once it stops.
273
+ *
274
+ * Nothing may scroll the column programmatically while it is gliding. A
275
+ * `scrollTo` issued against a running deceleration fights it, and the list
276
+ * stops dead somewhere between two rows and takes no further touches — a
277
+ * flick reads as the wheel freezing. So a correction that arrives mid-flight
278
+ * is remembered rather than performed, and applied at the moment the list
279
+ * comes to rest.
280
+ */
281
+ const moving = useRef(false);
282
+ const owedResync = useRef(false);
283
+ const settleTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
284
+
285
+ // The effect below closes over `index`; `atRest` runs from a scroll handler
286
+ // long after that render, so it needs the current one rather than the one it
287
+ // captured.
288
+ const latestIndex = useRef(index);
289
+ latestIndex.current = index;
290
+
291
+ const cancelTimer = useCallback(() => {
292
+ if (settleTimer.current === null) return;
293
+ clearTimeout(settleTimer.current);
294
+ settleTimer.current = null;
295
+ }, []);
296
+
297
+ /*
298
+ * Unanimated, and that is load-bearing rather than a matter of taste.
299
+ *
300
+ * An animated programmatic scroll raises the same begin and end momentum
301
+ * events a finger does, so the correction reports itself as a gesture, which
302
+ * commits, which corrects again — the column rides up and down and never
303
+ * settles. Placing it outright raises nothing, so the correction ends where
304
+ * it starts. It is also the honest movement: this is the row that was always
305
+ * holding, not a journey to it.
306
+ */
307
+ const snapTo = useCallback((to: number) => {
308
+ resting.current = to;
309
+ ref.current?.scrollTo({ y: to * ROW_HEIGHT, animated: false });
310
+ }, []);
311
+
312
+ /*
313
+ * Runs on the token as well as the index, and that is the whole point.
314
+ *
315
+ * A reported row can be refused — by `minTime`, by `maxTime`, or by a step
316
+ * the root rounds to. When it is, the value does not change, so `index` does
317
+ * not change either, and an effect watching the index alone never learns that
318
+ * anything happened. The column was left resting on a row the picker had not
319
+ * accepted, showing one time while reporting another, and no later drag could
320
+ * put it right: it had already recorded that row as where it sits.
321
+ *
322
+ * The token changes on every reported time, refused or not, so the column is
323
+ * always told to check itself, and the check is the same one it always was.
324
+ */
211
325
  useEffect(() => {
212
326
  if (index === resting.current) return;
213
- resting.current = index;
214
- ref.current?.scrollTo({ y: index * ROW_HEIGHT, animated: true });
215
- }, [index]);
216
-
217
- const settle = useCallback(
218
- (event: NativeSyntheticEvent<NativeScrollEvent>) => {
219
- const next = Math.round(event.nativeEvent.contentOffset.y / ROW_HEIGHT);
327
+ if (moving.current) {
328
+ owedResync.current = true;
329
+ return;
330
+ }
331
+ snapTo(index);
332
+ }, [index, syncToken, snapTo]);
333
+
334
+ /** The list has genuinely stopped. Pay off a correction if one is owed. */
335
+ const atRest = useCallback(() => {
336
+ cancelTimer();
337
+ moving.current = false;
338
+ if (!owedResync.current) return;
339
+ owedResync.current = false;
340
+ if (latestIndex.current !== resting.current) snapTo(latestIndex.current);
341
+ }, [cancelTimer, snapTo]);
342
+
343
+ const startMoving = useCallback(() => {
344
+ cancelTimer();
345
+ moving.current = true;
346
+ }, [cancelTimer]);
347
+
348
+ const settleAt = useCallback(
349
+ (y: number) => {
350
+ const next = Math.round(y / ROW_HEIGHT);
220
351
  const clamped = Math.min(Math.max(next, 0), items.length - 1);
221
352
  if (clamped === resting.current) return;
222
353
  resting.current = clamped;
354
+ /*
355
+ * This report supersedes any correction that was owed. The correction was
356
+ * worked out against a row the column has now left, and paying it off
357
+ * afterwards would drag the column back to a row nobody chose.
358
+ */
359
+ owedResync.current = false;
223
360
  selectionTick();
224
361
  onIndexChange(clamped);
225
362
  },
226
363
  [items.length, onIndexChange]
227
364
  );
228
365
 
366
+ /*
367
+ * A flick reports once, when it stops — not twice.
368
+ *
369
+ * Momentum begins a frame *after* the drag ends, so the end of a drag cannot
370
+ * tell a flick from a release by itself. It arms a short timer, and momentum
371
+ * starting cancels it; a drag that stops dead has no momentum to cancel it
372
+ * and settles on the timer. Reporting the release offset directly, as this
373
+ * used to, committed a row the finger was in the middle of flying past, and
374
+ * the picker then had two answers for one gesture.
375
+ */
376
+ const onDragEnd = useCallback(
377
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
378
+ const y = event.nativeEvent.contentOffset.y;
379
+ cancelTimer();
380
+ settleTimer.current = setTimeout(() => {
381
+ settleAt(y);
382
+ atRest();
383
+ }, 80);
384
+ },
385
+ [atRest, cancelTimer, settleAt]
386
+ );
387
+
388
+ const onMomentumEnd = useCallback(
389
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
390
+ cancelTimer();
391
+ settleAt(event.nativeEvent.contentOffset.y);
392
+ atRest();
393
+ },
394
+ [atRest, cancelTimer, settleAt]
395
+ );
396
+
397
+ useEffect(() => cancelTimer, [cancelTimer]);
398
+
229
399
  const selectedItem = items[index];
230
400
 
231
401
  return (
@@ -242,11 +412,14 @@ function Column<T>({
242
412
  snapToOffsets={snapToOffsets}
243
413
  disableIntervalMomentum
244
414
  decelerationRate="fast"
245
- /* Both, because only one of them fires: a flick ends in momentum, and a
246
- slow drag released on a snap point ends without any. Missing the drag
247
- case leaves a column that looks settled and has reported nothing. */
248
- onMomentumScrollEnd={settle}
249
- onScrollEndDrag={settle}
415
+ /* Both ends are wired, because only one of them fires: a flick ends in
416
+ momentum, and a slow drag released on a snap point ends without any.
417
+ Missing the drag case leaves a column that looks settled and has
418
+ reported nothing. */
419
+ onScrollBeginDrag={startMoving}
420
+ onScrollEndDrag={onDragEnd}
421
+ onMomentumScrollBegin={startMoving}
422
+ onMomentumScrollEnd={onMomentumEnd}
250
423
  /* The rows fade rather than unmount, so a recycled row would pop in at
251
424
  full opacity mid-scroll. A day of minutes is 60 views; keeping them
252
425
  all is cheaper than the flicker. */
@@ -284,11 +457,19 @@ function ColumnRow({
284
457
  }) {
285
458
  const slots = timePickerVariants();
286
459
 
460
+ /*
461
+ * The row either side of the centre stays a number you can read. It used to
462
+ * drop to just over half opacity and shrink by an eighth at one row out,
463
+ * which on a settling column reads as the digits smearing rather than as
464
+ * depth — and the neighbours are exactly the rows being compared against
465
+ * while choosing. The far rows still fall away, since that fall is what makes
466
+ * the column look like a wheel instead of a list.
467
+ */
287
468
  const style = useAnimatedStyle(() => {
288
469
  const distance = Math.abs(offset.value / ROW_HEIGHT - index);
289
470
  return {
290
- opacity: interpolate(distance, [0, 1, 2], [1, 0.55, 0.2], 'clamp'),
291
- transform: [{ scale: interpolate(distance, [0, 1], [1, 0.88], 'clamp') }],
471
+ opacity: interpolate(distance, [0, 1, 2], [1, 0.75, 0.25], 'clamp'),
472
+ transform: [{ scale: interpolate(distance, [0, 1], [1, 0.94], 'clamp') }],
292
473
  };
293
474
  });
294
475
 
@@ -311,8 +492,22 @@ function ColumnRow({
311
492
  interface FaceProps {
312
493
  value: TimeValue;
313
494
  onValueChange: (value: TimeValue) => void;
495
+ /**
496
+ * Bumped every time the root has looked at a reported time, so a face can
497
+ * tell "nothing changed because nothing was reported" from "nothing changed
498
+ * because what I reported was refused". Only the scrolling faces use it.
499
+ */
500
+ syncToken: number;
314
501
  hourCycle: HourCycle;
315
502
  minuteStep: number;
503
+ /**
504
+ * The span the picker will accept, so a face that lists whole times can
505
+ * offer only those. A face that does not list whole times — the wheel, whose
506
+ * columns each hold one part of one — ignores these and is corrected by the
507
+ * root instead.
508
+ */
509
+ minTime?: TimeValue;
510
+ maxTime?: TimeValue;
316
511
  locale?: string;
317
512
  disabled?: boolean;
318
513
  /** Ruler face only — the other two never draw a number of their own. */
@@ -322,6 +517,7 @@ interface FaceProps {
322
517
  function WheelFace({
323
518
  value,
324
519
  onValueChange,
520
+ syncToken,
325
521
  hourCycle,
326
522
  minuteStep,
327
523
  locale,
@@ -376,7 +572,17 @@ function WheelFace({
376
572
  const setMeridiem = useCallback(
377
573
  (index: number) => {
378
574
  const next = index === 0 ? 'am' : 'pm';
379
- if (next === meridiemOf(value.hour)) return;
575
+ /*
576
+ * Reported even when the column came back to the half it started on.
577
+ * Staying silent there left the root with nothing to answer, so a column
578
+ * dragged away and released short of a swap had no way to be told to sit
579
+ * back down — the other two columns had the same hole, and this is the
580
+ * same fix.
581
+ */
582
+ if (next === meridiemOf(value.hour)) {
583
+ onValueChange({ ...value });
584
+ return;
585
+ }
380
586
  onValueChange({
381
587
  ...value,
382
588
  hour: next === 'pm' ? value.hour + 12 : value.hour - 12,
@@ -397,6 +603,7 @@ function WheelFace({
397
603
  <Column
398
604
  items={hours}
399
605
  index={hourIndex}
606
+ syncToken={syncToken}
400
607
  onIndexChange={setHour}
401
608
  label={(hour) => (hourCycle === 24 ? padTwo(hour) : String(hour))}
402
609
  disabled={disabled}
@@ -406,6 +613,7 @@ function WheelFace({
406
613
  <Column
407
614
  items={minutes}
408
615
  index={minuteIndex}
616
+ syncToken={syncToken}
409
617
  onIndexChange={setMinute}
410
618
  label={padTwo}
411
619
  disabled={disabled}
@@ -416,6 +624,7 @@ function WheelFace({
416
624
  <Column
417
625
  items={meridiems}
418
626
  index={meridiemIndex}
627
+ syncToken={syncToken}
419
628
  onIndexChange={setMeridiem}
420
629
  label={(entry) => entry}
421
630
  disabled={disabled}
@@ -535,17 +744,16 @@ function handEnd(turn: number, length: number) {
535
744
  function ClockFaceLayout({
536
745
  value,
537
746
  onValueChange,
747
+ syncToken,
538
748
  hourCycle,
539
749
  minuteStep,
750
+ minTime,
751
+ maxTime,
540
752
  locale,
541
753
  disabled,
542
754
  }: FaceProps) {
543
755
  const slots = timePickerVariants();
544
- const times = useMemo(() => timesOfDay(minuteStep), [minuteStep]);
545
- const index = Math.min(
546
- times.length - 1,
547
- Math.round(timeToMinutes(value) / minuteStep)
548
- );
756
+ const { times, index } = useBoundedTimes(minuteStep, minTime, maxTime, value);
549
757
 
550
758
  const setIndex = useCallback(
551
759
  (next: number) => {
@@ -561,6 +769,7 @@ function ClockFaceLayout({
561
769
  <Column
562
770
  items={times}
563
771
  index={index}
772
+ syncToken={syncToken}
564
773
  onIndexChange={setIndex}
565
774
  label={(time) => formatTime(time, { hourCycle, locale })}
566
775
  disabled={disabled}
@@ -590,19 +799,18 @@ function ClockFaceLayout({
590
799
  function RulerFace({
591
800
  value,
592
801
  onValueChange,
802
+ syncToken,
593
803
  hourCycle,
594
804
  minuteStep,
805
+ minTime,
806
+ maxTime,
595
807
  locale,
596
808
  disabled,
597
809
  readout = 'default',
598
810
  }: FaceProps) {
599
811
  const slots = timePickerVariants({ readout });
600
812
  const ref = useRef<ScrollView>(null);
601
- const times = useMemo(() => timesOfDay(minuteStep), [minuteStep]);
602
- const index = Math.min(
603
- times.length - 1,
604
- Math.round(timeToMinutes(value) / minuteStep)
605
- );
813
+ const { times, index } = useBoundedTimes(minuteStep, minTime, maxTime, value);
606
814
  const resting = useRef(index);
607
815
  /*
608
816
  * Measured, not assumed: the scroller is narrower than the panel by whatever
@@ -622,18 +830,58 @@ function RulerFace({
622
830
  [times]
623
831
  );
624
832
 
833
+ // The scale runs the same rest-and-resync machinery the wheel's columns do,
834
+ // on the other axis and for the same two reasons: a refused tick moves
835
+ // nothing and has to be corrected, and a correction must never be scrolled
836
+ // into a scale that is still gliding.
837
+ const moving = useRef(false);
838
+ const owedResync = useRef(false);
839
+ const settleTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
840
+ const latestIndex = useRef(index);
841
+ latestIndex.current = index;
842
+
843
+ const cancelTimer = useCallback(() => {
844
+ if (settleTimer.current === null) return;
845
+ clearTimeout(settleTimer.current);
846
+ settleTimer.current = null;
847
+ }, []);
848
+
849
+ // Unanimated, for the reason given on the wheel's columns: an animated
850
+ // programmatic scroll reports itself as a gesture and the correction loops.
851
+ const snapTo = useCallback((to: number) => {
852
+ resting.current = to;
853
+ ref.current?.scrollTo({ x: to * TICK_SPACING, animated: false });
854
+ }, []);
855
+
625
856
  useEffect(() => {
626
857
  if (index === resting.current) return;
627
- resting.current = index;
628
- ref.current?.scrollTo({ x: index * TICK_SPACING, animated: true });
629
- }, [index]);
630
-
631
- const settle = useCallback(
632
- (event: NativeSyntheticEvent<NativeScrollEvent>) => {
633
- const next = Math.round(event.nativeEvent.contentOffset.x / TICK_SPACING);
858
+ if (moving.current) {
859
+ owedResync.current = true;
860
+ return;
861
+ }
862
+ snapTo(index);
863
+ }, [index, syncToken, snapTo]);
864
+
865
+ const atRest = useCallback(() => {
866
+ cancelTimer();
867
+ moving.current = false;
868
+ if (!owedResync.current) return;
869
+ owedResync.current = false;
870
+ if (latestIndex.current !== resting.current) snapTo(latestIndex.current);
871
+ }, [cancelTimer, snapTo]);
872
+
873
+ const startMoving = useCallback(() => {
874
+ cancelTimer();
875
+ moving.current = true;
876
+ }, [cancelTimer]);
877
+
878
+ const settleAt = useCallback(
879
+ (x: number) => {
880
+ const next = Math.round(x / TICK_SPACING);
634
881
  const clamped = Math.min(Math.max(next, 0), times.length - 1);
635
882
  if (clamped === resting.current) return;
636
883
  resting.current = clamped;
884
+ owedResync.current = false;
637
885
  selectionTick();
638
886
  const time = times[clamped];
639
887
  if (time) onValueChange(time);
@@ -641,6 +889,29 @@ function RulerFace({
641
889
  [onValueChange, times]
642
890
  );
643
891
 
892
+ const onDragEnd = useCallback(
893
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
894
+ const x = event.nativeEvent.contentOffset.x;
895
+ cancelTimer();
896
+ settleTimer.current = setTimeout(() => {
897
+ settleAt(x);
898
+ atRest();
899
+ }, 80);
900
+ },
901
+ [atRest, cancelTimer, settleAt]
902
+ );
903
+
904
+ const onMomentumEnd = useCallback(
905
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
906
+ cancelTimer();
907
+ settleAt(event.nativeEvent.contentOffset.x);
908
+ atRest();
909
+ },
910
+ [atRest, cancelTimer, settleAt]
911
+ );
912
+
913
+ useEffect(() => cancelTimer, [cancelTimer]);
914
+
644
915
  const stepsPerHour = Math.max(1, Math.round(60 / minuteStep));
645
916
 
646
917
  /*
@@ -678,8 +949,10 @@ function RulerFace({
678
949
  snapToOffsets={snapToOffsets}
679
950
  disableIntervalMomentum
680
951
  decelerationRate="fast"
681
- onMomentumScrollEnd={settle}
682
- onScrollEndDrag={settle}
952
+ onScrollBeginDrag={startMoving}
953
+ onScrollEndDrag={onDragEnd}
954
+ onMomentumScrollBegin={startMoving}
955
+ onMomentumScrollEnd={onMomentumEnd}
683
956
  accessibilityRole="adjustable"
684
957
  accessibilityLabel="Time"
685
958
  accessibilityValue={{ text: formatTime(value, { hourCycle, locale }) }}
@@ -879,10 +1152,29 @@ function TimePickerRoot({
879
1152
  * itself using — putting both here means every layout is bounded by the same
880
1153
  * rule and none of them has to carry it.
881
1154
  */
1155
+ /*
1156
+ * Counts refusals, not reports.
1157
+ *
1158
+ * A face has no way of knowing that the row it landed on was bounded away by
1159
+ * `minTime` or `maxTime`, or rounded to a different one by the step — from
1160
+ * where it stands, a refusal and a value that simply did not move are the
1161
+ * same silence. This changes when a report was refused, which is the one
1162
+ * thing the faces were missing, and is what lets a column put itself back on
1163
+ * the row that actually holds.
1164
+ *
1165
+ * Only on refusal, though. Bumping it on every report re-rendered all three
1166
+ * columns each time any one of them settled, for no answer any of them
1167
+ * needed.
1168
+ */
1169
+ const [refusals, setRefusals] = useState(0);
1170
+
882
1171
  const commit = useCallback(
883
1172
  (next: TimeValue) => {
884
1173
  const bounded = clampTime(roundToStep(next, step), minTime, maxTime);
885
- if (isSameTime(bounded, selected)) return;
1174
+ if (isSameTime(bounded, selected)) {
1175
+ setRefusals((count) => count + 1);
1176
+ return;
1177
+ }
886
1178
  if (!isValueControlled) setInternalValue(bounded);
887
1179
  onValueChange?.(bounded);
888
1180
  },
@@ -898,7 +1190,10 @@ function TimePickerRoot({
898
1190
  const face: FaceProps = {
899
1191
  value: draft,
900
1192
  onValueChange: commit,
1193
+ syncToken: refusals,
901
1194
  hourCycle,
1195
+ minTime,
1196
+ maxTime,
902
1197
  minuteStep: step,
903
1198
  locale,
904
1199
  disabled,
package/src/index.ts CHANGED
@@ -106,6 +106,21 @@ export {
106
106
  type BarChartStatus,
107
107
  type BarChartOrientation,
108
108
  } from './components/bar-chart';
109
+ export {
110
+ CandlestickChart,
111
+ useCandlestickChart,
112
+ type CandlestickChartProps,
113
+ type CandlestickChartHandle,
114
+ type CandlestickChartHeaderProps,
115
+ type CandlestickChartGridProps,
116
+ type CandlestickChartCandlesProps,
117
+ type CandlestickChartXAxisProps,
118
+ type CandlestickChartYAxisProps,
119
+ type CandlestickChartTooltipProps,
120
+ type CandlestickChartLegendProps,
121
+ type CandlestickChartDatum,
122
+ type CandlestickChartStatus,
123
+ } from './components/candlestick-chart';
109
124
  export {
110
125
  BottomSheet,
111
126
  type BottomSheetProps,
@@ -382,6 +397,15 @@ export {
382
397
  type ComboboxSelection,
383
398
  type ComboboxPresentation,
384
399
  } from './components/combobox';
400
+ export {
401
+ ContextMenu,
402
+ type ContextMenuProps,
403
+ type ContextMenuTriggerProps,
404
+ type ContextMenuContentProps,
405
+ type ContextMenuItemProps,
406
+ type ContextMenuPreviewProps,
407
+ type ContextMenuAnchor,
408
+ } from './components/context-menu';
385
409
  export {
386
410
  Drawer,
387
411
  type DrawerProps,
@@ -490,6 +514,14 @@ export {
490
514
  type PieDatum,
491
515
  } from './components/pie-chart';
492
516
  export { Textarea, type TextareaProps } from './components/textarea';
517
+ export {
518
+ TagInput,
519
+ type TagInputProps,
520
+ type TagInputVariant,
521
+ type TagInputSize,
522
+ type TagBlurBehavior,
523
+ type TagRejection,
524
+ } from './components/tag-input';
493
525
  export {
494
526
  Item,
495
527
  type ItemProps,
@@ -608,6 +640,7 @@ export {
608
640
  } from './components/message';
609
641
  export {
610
642
  Popover,
643
+ usePopoverAnchor,
611
644
  type PopoverProps,
612
645
  type PopoverTriggerProps,
613
646
  type PopoverContentProps,
@@ -616,6 +649,8 @@ export {
616
649
  type PopoverPlacement,
617
650
  type PopoverAlign,
618
651
  type PopoverPresentation,
652
+ type PopoverAnchorRect,
653
+ type PopoverAnchorControls,
619
654
  } from './components/popover';
620
655
  export {
621
656
  Post,