panelui-native 0.46.0 → 0.49.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 (72) hide show
  1. package/README.md +5 -1
  2. package/lib/module/components/accordion/index.js +32 -4
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/button/index.js +83 -15
  5. package/lib/module/components/button/index.js.map +1 -1
  6. package/lib/module/components/button-group/index.js +186 -0
  7. package/lib/module/components/button-group/index.js.map +1 -0
  8. package/lib/module/components/color-picker/index.js +110 -1
  9. package/lib/module/components/color-picker/index.js.map +1 -1
  10. package/lib/module/components/date-time-picker/index.js +23 -5
  11. package/lib/module/components/date-time-picker/index.js.map +1 -1
  12. package/lib/module/components/fab/index.js +514 -0
  13. package/lib/module/components/fab/index.js.map +1 -0
  14. package/lib/module/components/markdown-editor/index.js +406 -0
  15. package/lib/module/components/markdown-editor/index.js.map +1 -0
  16. package/lib/module/components/markdown-editor/markdown-transforms.js +243 -0
  17. package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -0
  18. package/lib/module/components/questionnaire/index.js +1312 -0
  19. package/lib/module/components/questionnaire/index.js.map +1 -0
  20. package/lib/module/components/tabs/index.js +94 -18
  21. package/lib/module/components/tabs/index.js.map +1 -1
  22. package/lib/module/components/time-picker/index.js +34 -6
  23. package/lib/module/components/time-picker/index.js.map +1 -1
  24. package/lib/module/components/tree/index.js +500 -0
  25. package/lib/module/components/tree/index.js.map +1 -0
  26. package/lib/module/icons/index.js +217 -0
  27. package/lib/module/icons/index.js.map +1 -1
  28. package/lib/module/index.js +6 -1
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/typescript/src/components/accordion/index.d.ts +21 -0
  31. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  32. package/lib/typescript/src/components/button/index.d.ts +21 -0
  33. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  34. package/lib/typescript/src/components/button-group/index.d.ts +212 -0
  35. package/lib/typescript/src/components/button-group/index.d.ts.map +1 -0
  36. package/lib/typescript/src/components/color-picker/index.d.ts +82 -1
  37. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
  38. package/lib/typescript/src/components/date-time-picker/index.d.ts +7 -1
  39. package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -1
  40. package/lib/typescript/src/components/fab/index.d.ts +285 -0
  41. package/lib/typescript/src/components/fab/index.d.ts.map +1 -0
  42. package/lib/typescript/src/components/markdown-editor/index.d.ts +102 -0
  43. package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -0
  44. package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +76 -0
  45. package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts.map +1 -0
  46. package/lib/typescript/src/components/questionnaire/index.d.ts +336 -0
  47. package/lib/typescript/src/components/questionnaire/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/tabs/index.d.ts +9 -0
  49. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  50. package/lib/typescript/src/components/time-picker/index.d.ts +19 -1
  51. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  52. package/lib/typescript/src/components/tree/index.d.ts +125 -0
  53. package/lib/typescript/src/components/tree/index.d.ts.map +1 -0
  54. package/lib/typescript/src/icons/index.d.ts +22 -0
  55. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  56. package/lib/typescript/src/index.d.ts +8 -3
  57. package/lib/typescript/src/index.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/components/accordion/index.tsx +48 -6
  60. package/src/components/button/index.tsx +97 -15
  61. package/src/components/button-group/index.tsx +199 -0
  62. package/src/components/color-picker/index.tsx +140 -3
  63. package/src/components/date-time-picker/index.tsx +39 -3
  64. package/src/components/fab/index.tsx +583 -0
  65. package/src/components/markdown-editor/index.tsx +526 -0
  66. package/src/components/markdown-editor/markdown-transforms.ts +228 -0
  67. package/src/components/questionnaire/index.tsx +1615 -0
  68. package/src/components/tabs/index.tsx +100 -28
  69. package/src/components/time-picker/index.tsx +42 -6
  70. package/src/components/tree/index.tsx +564 -0
  71. package/src/icons/index.tsx +154 -0
  72. package/src/index.ts +77 -1
@@ -36,6 +36,7 @@ import {
36
36
  type ReactNode,
37
37
  } from 'react';
38
38
  import {
39
+ Platform,
39
40
  Pressable,
40
41
  ScrollView,
41
42
  View,
@@ -92,8 +93,18 @@ const SWIPE_RESISTANCE_AT_END = 0.16;
92
93
  * card: a panel on its way out is on its way out, and the one arriving comes
93
94
  * back up through the same fade. The two together read as a dissolve carried
94
95
  * by the movement, rather than as a gap between two slides.
96
+ *
97
+ * **iOS only, and that is not a compromise.** A transform is a property of the
98
+ * layer the panel is drawn into, so moving it costs nothing per frame however
99
+ * much is inside it. An opacity that changes every frame is not: on Android the
100
+ * panel's view group has no offscreen buffer to fade, so the alpha is pushed
101
+ * down into its children and the whole visible subtree is re-drawn on every
102
+ * frame of the drag. Behind a list that is thirty rows of text and images,
103
+ * which is what a tab panel usually is, that is the difference between a swipe
104
+ * that tracks the finger and one that stutters. iOS fades the layer itself and
105
+ * is unaffected.
95
106
  */
96
- const SWIPE_FADE = 0.75;
107
+ const SWIPE_FADE = Platform.OS === 'ios' ? 0.75 : 0;
97
108
 
98
109
  /**
99
110
  * How far off its resting place a panel arriving by a *press* starts, as a
@@ -101,7 +112,7 @@ const SWIPE_FADE = 0.75;
101
112
  *
102
113
  * A press has no finger travel to continue from, so it is given a throw of its
103
114
  * own. A swipe does not use this: the panel it hands over to picks up exactly
104
- * where the outgoing one was let go — see `setValue`.
115
+ * one width from wherever the outgoing one has travelled to — see `setValue`.
105
116
  */
106
117
  const SWIPE_ENTER = 0.3;
107
118
 
@@ -291,6 +302,15 @@ export interface TabsProps extends ViewProps {
291
302
  * already wants a horizontal drag — a carousel, a slider, a row that swipes
292
303
  * open — and the two cannot both have it. Turn it on for panels of ordinary
293
304
  * scrolling content, where it is the gesture people try first.
305
+ *
306
+ * **It does not change what is mounted.** Only `keepMounted` decides that,
307
+ * with or without this — a swipe animates between two panels of which one is
308
+ * being unmounted and the other mounted for the first time, exactly as a
309
+ * press does. What it does change is that the mount now happens *while
310
+ * something is moving*, so a panel that is slow to build stops being a pause
311
+ * before it appears and starts being a stutter in the movement. If a swipe
312
+ * feels heavier than a press on the same tab set, the panel is expensive to
313
+ * mount; `keepMounted` is the answer, not turning this off.
294
314
  */
295
315
  swipeable?: boolean;
296
316
  children: ReactNode;
@@ -319,8 +339,26 @@ function TabsRoot({
319
339
  const isControlled = value !== undefined;
320
340
  const resolvedValue = isControlled ? value : internalValue;
321
341
 
342
+ /*
343
+ * The two things `setValue` reads that change on every switch, held where
344
+ * reading them does not make it a new function.
345
+ *
346
+ * With them in the dependency list, changing tab produced a new `setValue`,
347
+ * therefore a new context, therefore a new `step`, therefore a new pan
348
+ * gesture — which detaches and re-attaches the native recogniser on the same
349
+ * commit that mounts the arriving panel's contents. The busiest frame of the
350
+ * interaction was doing the one piece of work that had no reason to be there.
351
+ */
352
+ const tabsRef = useRef(tabs);
353
+ tabsRef.current = tabs;
354
+ const valueRef = useRef(resolvedValue);
355
+ valueRef.current = resolvedValue;
356
+
322
357
  const setValue = useCallback(
323
358
  (next: string, handover?: SwipeHandover) => {
359
+ const tabs = tabsRef.current;
360
+ const resolvedValue = valueRef.current;
361
+
324
362
  /*
325
363
  * The arriving panel starts on the side it is arriving from, and travels
326
364
  * in. Done here rather than in the panel because only the root knows
@@ -338,11 +376,13 @@ function TabsRoot({
338
376
  if (handover) {
339
377
  /*
340
378
  * A swipe hands over mid-movement, so the arriving panel starts a
341
- * whole panel's width from wherever the outgoing one was let go —
379
+ * whole panel's width from wherever the outgoing one has got to —
342
380
  * which is where it *would* have been all along, had both been
343
- * mounted. Adding to the displacement rather than replacing it is
344
- * the entire difference between one continuous movement and a jump
345
- * at the moment the finger lifts.
381
+ * mounted. Read live rather than captured, because the outgoing
382
+ * panel is still travelling while this runs: adding to the current
383
+ * displacement rather than replacing it is the whole difference
384
+ * between one continuous movement and a jump at the moment the
385
+ * arriving panel finishes mounting.
346
386
  */
347
387
  swipeOffset.value += direction * sign * panelWidth.value;
348
388
  swipeOffset.value = withSpring(0, {
@@ -361,16 +401,7 @@ function TabsRoot({
361
401
  if (!isControlled) setInternalValue(next);
362
402
  onValueChange?.(next);
363
403
  },
364
- [
365
- isControlled,
366
- onValueChange,
367
- swipeable,
368
- tabs,
369
- resolvedValue,
370
- sign,
371
- swipeOffset,
372
- panelWidth,
373
- ]
404
+ [isControlled, onValueChange, swipeable, sign, swipeOffset, panelWidth]
374
405
  );
375
406
 
376
407
  const registerLayout = useCallback((tab: string, layout: TabLayout) => {
@@ -781,13 +812,25 @@ function TabsContent({ className, value, children, style, ...props }: TabsConten
781
812
  count.value = tabs.length;
782
813
  }, [position, tabs.length, index, count]);
783
814
 
815
+ /*
816
+ * Same reason as `setValue` on the root: `step` is reached through the pan
817
+ * gesture, and a `step` that changes identity on every switch rebuilds the
818
+ * gesture on every switch. The two facts it needs are read at call time
819
+ * instead, which is when they are wanted anyway.
820
+ */
821
+ const tabsRef = useRef(tabs);
822
+ tabsRef.current = tabs;
823
+ const valueRef = useRef(value);
824
+ valueRef.current = value;
825
+
784
826
  const step = useCallback(
785
827
  (delta: number, velocity: number) => {
786
- const from = tabs.indexOf(value);
787
- const next = tabs[from + delta];
828
+ const list = tabsRef.current;
829
+ const from = list.indexOf(valueRef.current);
830
+ const next = list[from + delta];
788
831
  if (next) setValue(next, { velocity });
789
832
  },
790
- [tabs, value, setValue]
833
+ [setValue]
791
834
  );
792
835
 
793
836
  const pan = useMemo(
@@ -828,14 +871,31 @@ function TabsContent({ className, value, children, style, ...props }: TabsConten
828
871
  // Distance and speed can disagree — a flick back the way it came
829
872
  // reads as a cancel — so the direction comes from whichever of the
830
873
  // two crossed its threshold, speed first.
831
- //
832
- // No spring back to zero here: `setValue` on the root carries the
833
- // displacement across to the arriving panel, and springing to rest
834
- // first would undo it a frame before it happens.
835
- runOnJS(step)(
836
- fast ? (speed < 0 ? 1 : -1) : travel < 0 ? 1 : -1,
837
- event.velocityX
838
- );
874
+ const delta = fast ? (speed < 0 ? 1 : -1) : travel < 0 ? 1 : -1;
875
+
876
+ /*
877
+ * The outgoing panel carries on off the edge on the UI thread,
878
+ * immediately, rather than waiting to be told what happened.
879
+ *
880
+ * It used to hold wherever the finger left it until `setValue`
881
+ * landed — which is fine when React commits in a frame, and is a
882
+ * visible freeze when the arriving panel is expensive to mount, a
883
+ * list of any size being the usual case. The panel appeared to
884
+ * stick to the screen for exactly as long as the JS thread was
885
+ * busy, which reads as the swipe having dropped the gesture.
886
+ *
887
+ * `setValue` still places the arriving panel *relative* to this
888
+ * one, reading the offset live at commit time, so continuing the
889
+ * movement here does not desynchronise the handover: the panel
890
+ * that arrives is one width from wherever this one has got to,
891
+ * whenever that turns out to be.
892
+ */
893
+ offset.value = withSpring(-delta * sign * width.value, {
894
+ ...ENTER_SPRING,
895
+ velocity: event.velocityX,
896
+ });
897
+
898
+ runOnJS(step)(delta, event.velocityX);
839
899
  return;
840
900
  }
841
901
 
@@ -853,6 +913,11 @@ function TabsContent({ className, value, children, style, ...props }: TabsConten
853
913
  );
854
914
 
855
915
  const followStyle = useAnimatedStyle(() => {
916
+ // No `opacity` key at all when there is no fade, rather than a constant 1.
917
+ // Declaring it would still mark the property as animated and hand the view
918
+ // an alpha to composite every frame — the cost this is avoiding.
919
+ if (!SWIPE_FADE) return { transform: [{ translateX: offset.value }] };
920
+
856
921
  const span = width.value || 1;
857
922
  const travelled = Math.min(1, Math.abs(offset.value) / span);
858
923
  return {
@@ -893,7 +958,14 @@ function TabsContent({ className, value, children, style, ...props }: TabsConten
893
958
  const measured = event.nativeEvent.layout.width;
894
959
  if (measured > 0) width.value = measured;
895
960
  }}
896
- style={[!active && { display: 'none' }, swipeable && followStyle, style]}
961
+ /*
962
+ * The follow style goes on the panel that is moving, and only that one.
963
+ * Under `keepMounted` every other panel is `display: none` and cannot be
964
+ * seen to move — but an animated style still subscribes them all to the
965
+ * offset, so a five-tab set ran five mappers per frame to reposition four
966
+ * views nobody was looking at.
967
+ */
968
+ style={[!active && { display: 'none' }, swipeable && active && followStyle, style]}
897
969
  pointerEvents={active ? 'auto' : 'none'}
898
970
  accessibilityElementsHidden={!active}
899
971
  importantForAccessibility={active ? 'auto' : 'no-hide-descendants'}
@@ -116,6 +116,15 @@ const TICK_SPACING = 12;
116
116
  /** Settles the clock hands after a pick. Slow enough to be followed by eye. */
117
117
  const HAND_SPRING = { damping: 16, stiffness: 140, mass: 0.7 } as const;
118
118
 
119
+ /**
120
+ * How loudly the ruler face states the time it is on.
121
+ *
122
+ * `default` is the big centred number, right when the scale is the only thing
123
+ * on the panel. `compact` steps it down to sit under something that outranks
124
+ * it, and `none` drops it for a caller that writes the time itself.
125
+ */
126
+ export type TimePickerReadout = 'default' | 'compact' | 'none';
127
+
119
128
  const timePickerVariants = tv({
120
129
  slots: {
121
130
  panel: 'gap-3',
@@ -128,10 +137,20 @@ const timePickerVariants = tv({
128
137
  row: 'items-center justify-center',
129
138
  rowLabel: 'text-lg tabular-nums text-foreground',
130
139
  rowLabelSelected: 'font-semibold text-primary',
131
- readout: 'text-center text-4xl font-semibold tabular-nums text-foreground',
140
+ readout: 'text-center font-semibold tabular-nums text-foreground',
132
141
  clock: 'flex-row items-center gap-4',
133
142
  footer: 'flex-row justify-end gap-2',
134
143
  },
144
+ variants: {
145
+ readout: {
146
+ default: { readout: 'text-4xl' },
147
+ compact: { readout: 'text-xl' },
148
+ none: {},
149
+ },
150
+ },
151
+ defaultVariants: {
152
+ readout: 'default',
153
+ },
135
154
  });
136
155
 
137
156
  /* -------------------------------------------------------------------------- */
@@ -296,6 +315,8 @@ interface FaceProps {
296
315
  minuteStep: number;
297
316
  locale?: string;
298
317
  disabled?: boolean;
318
+ /** Ruler face only — the other two never draw a number of their own. */
319
+ readout?: TimePickerReadout;
299
320
  }
300
321
 
301
322
  function WheelFace({
@@ -573,8 +594,9 @@ function RulerFace({
573
594
  minuteStep,
574
595
  locale,
575
596
  disabled,
597
+ readout = 'default',
576
598
  }: FaceProps) {
577
- const slots = timePickerVariants();
599
+ const slots = timePickerVariants({ readout });
578
600
  const ref = useRef<ScrollView>(null);
579
601
  const times = useMemo(() => timesOfDay(minuteStep), [minuteStep]);
580
602
  const index = Math.min(
@@ -638,10 +660,12 @@ function RulerFace({
638
660
  );
639
661
 
640
662
  return (
641
- <View className="gap-4">
642
- <Text className={slots.readout()}>
643
- {formatTime(value, { hourCycle, locale })}
644
- </Text>
663
+ <View className={readout === 'none' ? undefined : 'gap-4'}>
664
+ {readout === 'none' ? null : (
665
+ <Text className={slots.readout()}>
666
+ {formatTime(value, { hourCycle, locale })}
667
+ </Text>
668
+ )}
645
669
 
646
670
  <View className="relative h-14 justify-center" onLayout={onLayout}>
647
671
  <ScrollView
@@ -768,6 +792,16 @@ export interface TimePickerProps {
768
792
  format?: (value: TimeValue) => string;
769
793
  /** BCP 47 tag for the time's text and the meridiem labels. */
770
794
  locale?: string;
795
+ /**
796
+ * How loudly the `ruler` face states the time it is on. The other two faces
797
+ * spell the time out in their own columns and hands, and ignore this.
798
+ *
799
+ * The big centred number is right when the scale is the only thing on the
800
+ * panel. Under something that outranks it — a calendar, a form row — it is
801
+ * the largest text on screen for the smaller half of the answer, so step it
802
+ * down with `compact` or take it over yourself with `none`.
803
+ */
804
+ readout?: TimePickerReadout;
771
805
  /** Stop the trigger opening it, and the faces from being scrolled. */
772
806
  disabled?: boolean;
773
807
  className?: string;
@@ -803,6 +837,7 @@ function TimePickerRoot({
803
837
  placeholder = DEFAULT_PLACEHOLDER,
804
838
  format,
805
839
  locale,
840
+ readout = 'default',
806
841
  disabled = false,
807
842
  className,
808
843
  children,
@@ -867,6 +902,7 @@ function TimePickerRoot({
867
902
  minuteStep: step,
868
903
  locale,
869
904
  disabled,
905
+ readout,
870
906
  };
871
907
 
872
908
  if (presentation === 'inline') {